Money review: Bazaar rejection no longer burns the purchase token, read-only order admin, pack grant from order snapshot
- Bazaar verify: a rejected token is not stored on the order or the failed transaction, so the same purchase can still pay the right order; a refunded order refuses a new token (409). - OrderAdmin: every field read-only, no add/delete; Transaction admin read-only. - grant_pack uses the order's snapshot when the plan was deleted, logs ERROR when nothing is granted. - Pay link: authority is stored only if the order amount did not change during PaymentRequest. - PlanAdmin accepts only IRT (the app always renders toman). - reconcile_pending_order logs unexpected exceptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from django.conf import settings
|
||||
@@ -28,6 +29,8 @@ from .serializers import (
|
||||
)
|
||||
from .services import apply_order_payment_result, create_order, pay_link_is_valid, reconcile_pending_order
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _redirect_to_configured_url(url: str) -> HttpResponseRedirect:
|
||||
"""
|
||||
@@ -178,24 +181,32 @@ class PaymentRedirectView(APIView):
|
||||
raise Http404
|
||||
order = get_object_or_404(Order, pk=order_id, provider='gateway')
|
||||
|
||||
# authority فقط وقتی ذخیره میشود که هنوز خالی است (درخواستِ همزمانِ دیگری زودتر نساخته باشد)
|
||||
# و مبلغِ سفارش همانی است که به زرینپال گفته شد. کدِ تخفیفی که وسطِ PaymentRequest ثبت شود
|
||||
# مبلغ را کم میکرد و authority با مبلغِ قبلی مینشست: کاربر مبلغِ کامل را میپرداخت و verify با
|
||||
# مبلغِ تازه رد میشد (بازتولید شد). در آن حالت یک بار دیگر با مبلغِ تازه درخواست میشود.
|
||||
for _attempt in range(2):
|
||||
if order.status != 'pending' or order.gateway_authority:
|
||||
break
|
||||
requested_amount = order.amount
|
||||
result = zarinpal.request_payment(order)
|
||||
Order.objects.filter(pk=order.pk, gateway_authority='', amount=requested_amount).update(
|
||||
gateway_authority=result.authority,
|
||||
)
|
||||
order.refresh_from_db(fields=['gateway_authority', 'amount', 'status'])
|
||||
|
||||
if order.status != 'pending':
|
||||
# سفارش از قبل نتیجه گرفته (paid/failed)؛ چیزی برای پرداختِ تازه نیست.
|
||||
# سفارش از قبل نتیجه گرفته (paid/failed، یا کدِ ۱۰۰٪ همین الان)؛ چیزی برای پرداختِ تازه نیست.
|
||||
# کاربر به اپ برمیگردد با همان نتیجه، نه به ریشهی دامنهی API.
|
||||
if settings.ZARINPAL_REDIRECT_URL:
|
||||
return _redirect_to_configured_url(
|
||||
f'{settings.ZARINPAL_REDIRECT_URL}?orderId={order.id}&status={order.status}'
|
||||
)
|
||||
return HttpResponseRedirect(settings.PUBLIC_BASE_URL or '/')
|
||||
|
||||
if not order.gateway_authority:
|
||||
result = zarinpal.request_payment(order)
|
||||
# ⚠️ محافظت در برابرِ race: اگه بینِ خواندنِ بالا و اینجا یک
|
||||
# درخواستِ همزمانِ دیگر authority ساخته باشد، همان مقدارِ اول را
|
||||
# نگه میداریم، نه این result تازه را (فقط وقتی هنوز خالی است آپدیت میشود).
|
||||
Order.objects.filter(pk=order.pk, gateway_authority='').update(
|
||||
gateway_authority=result.authority,
|
||||
)
|
||||
order.refresh_from_db(fields=['gateway_authority'])
|
||||
logger.error('pay link: amount of order %s kept changing during PaymentRequest', order.id)
|
||||
return Response({'detail': 'مبلغِ سفارش در حینِ رفتن به درگاه عوض شد؛ دوباره «پرداخت» را بزن.'},
|
||||
status=status.HTTP_409_CONFLICT)
|
||||
|
||||
return HttpResponseRedirect(f'{settings.ZARINPAL_STARTPAY_BASE}/{order.gateway_authority}')
|
||||
|
||||
@@ -359,13 +370,17 @@ class BazaarVerifyPurchaseView(APIView):
|
||||
if self._second_purchase_on_paid_order(order, v['purchaseToken']):
|
||||
return self._second_purchase_response()
|
||||
new_status = 'paid' if result.is_valid else 'failed'
|
||||
order.bazaar_purchase_token = v['purchaseToken']
|
||||
order.save(update_fields=['bazaar_purchase_token'])
|
||||
# توکن فقط با پاسخِ «معتبر» روی سفارش و تراکنش مینشیند. قیدِ یکتای (gateway, ref) همهی
|
||||
# وضعیتها را میگیرد؛ وقتی ردِ بازار (not_found، یا توکن با سفارشِ اشتباه) هم توکن را ثبت
|
||||
# میکرد، همان خرید دیگر روی هیچ سفارشی، حتی همین، «paid» نمیشد (بازتولید شد).
|
||||
if result.is_valid:
|
||||
order.bazaar_purchase_token = v['purchaseToken']
|
||||
order.save(update_fields=['bazaar_purchase_token'])
|
||||
|
||||
apply_order_payment_result(
|
||||
order=order,
|
||||
gateway='bazaar',
|
||||
gateway_ref_id=v['purchaseToken'],
|
||||
gateway_ref_id=v['purchaseToken'] if result.is_valid else '',
|
||||
new_status=new_status,
|
||||
event='bazaar_verify',
|
||||
)
|
||||
@@ -382,7 +397,9 @@ class BazaarVerifyPurchaseView(APIView):
|
||||
خریدِ دیگری برای سفارشی که با یک خریدِ دیگر پرداخت شده. پیشتر پاسخ «paid» بود، چیزی داده
|
||||
نمیشد و توکنِ دوم هم برای هر سفارشِ بعدی سوخته بود؛ کاربر دو بار پول داده بود.
|
||||
"""
|
||||
return (order.status == 'paid' and bool(order.bazaar_purchase_token)
|
||||
# «refunded» هم: سفارشِ بازپرداختشده با توکنِ تازه دوباره «paid» میشد ولی اعتبارش (که یک بار
|
||||
# برای همین سفارش داده شده بود) دیگر داده نمیشد؛ کاربر پولِ خریدِ دوم را بیهیچ چیزی میداد.
|
||||
return (order.status in ('paid', 'refunded') and bool(order.bazaar_purchase_token)
|
||||
and order.bazaar_purchase_token != purchase_token)
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user