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:
2026-10-05 09:26:26 +03:30
parent eb4e97753b
commit fedd10ff22
7 changed files with 268 additions and 22 deletions

View File

@@ -8,6 +8,13 @@ from .models import BazaarCredential, Order, Transaction
class TransactionInline(admin.TabularInline):
model = Transaction
extra = 0
can_delete = False
def has_add_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
@admin.register(Order)
@@ -21,10 +28,23 @@ class OrderAdmin(admin.ModelAdmin):
list_display = ('id', 'user_id', 'plan_title', 'amount', 'currency', 'status', 'provider', 'created_at')
list_filter = ('status', 'provider', 'currency')
search_fields = ('=id', '=user_id')
readonly_fields = ('status',)
inlines = [TransactionInline]
actions = ['cancel_pending', 'refund']
# همه‌ی ستون‌ها فقط‌خواندنی‌اند، نه فقط وضعیت: اجازه‌ی change برای دو action لازم است، و تا امروز
# همان اجازه فرمِ سفارش را هم باز می‌کرد. کارمندی که فقط باید لغو/بازپرداخت کند، روی سفارشِ در
# انتظارِ خودش amount را ۱۰۰۰ و question_credits را ۱۰۰٬۰۰۰ کرد و با ۱۰۰۰ تومان ۱۰۰٬۰۰۰ سوال گرفت
# (بازتولید شد). سفارش سندِ مالی است: از پنل ساخته یا پاک هم نمی‌شود؛ پاک شدنِ سفارشِ پرداخت‌شده
# تراکنش‌هایش را می‌برد و توکنِ خریدش برای سفارشِ دیگری دوباره پذیرفته می‌شد.
def get_readonly_fields(self, request, obj=None):
return [f.name for f in self.model._meta.fields]
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
@admin.action(description='لغوِ سفارشِ در انتظار', permissions=['change'])
def cancel_pending(self, request, queryset):
from .services import OrderStateError, cancel_pending_order
@@ -69,9 +89,21 @@ class OrderAdmin(admin.ModelAdmin):
@admin.register(Transaction)
class TransactionAdmin(admin.ModelAdmin):
"""فقط برای دیدن؛ تراکنش را فقط تأییدِ درگاه و actionهای سفارش می‌سازند (قیدِ یکتای gateway_ref_id
جلوی تکرارِ توکنِ خرید است و ویرایش یا حذفِ دستیِ ردیف آن را برمی‌داشت)."""
list_display = ('id', 'order', 'gateway', 'status', 'paid_at')
list_filter = ('gateway', 'status')
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
@admin.register(BazaarCredential)
class BazaarCredentialAdmin(admin.ModelAdmin):