admin: cancel a pending order, refund a paid one

There was no way to take an order back: staff could only edit the
status by hand, and turning 'paid' into 'failed' left the credits,
heart days or subscription days the order had given. Two OrderAdmin
actions now do it:
- cancel a pending order (a payment that does arrive later still counts);
- refund a paid order, after a confirmation page: takes back the pack's
  unspent credits (never below zero), its heart days, or the plan's
  subscription days, and undoes the referrer's commission.
The money itself is returned in the Zarinpal / Bazaar panel; the action
says so. Status is read-only in the order form. Steps in
docs/operations.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 22:37:30 +03:30
parent 4700ccbf91
commit d7d168e82f
10 changed files with 401 additions and 4 deletions

View File

@@ -12,9 +12,59 @@ class TransactionInline(admin.TabularInline):
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ('id', 'user_id', 'subscription_id', 'amount', 'currency', 'status', 'provider', 'created_at')
"""
لغو و بازپرداخت فقط با دو action (از ۲۰۲۶-۱۰-۰۲)، نه با عوض کردنِ دستیِ وضعیت: «paid» را با دست
«failed» کردن اعتبار یا اشتراکی را که سفارش داده بود پس نمی‌گرفت. برای همین وضعیت این‌جا فقط‌خواندنی است.
منطق در payment.services (cancel_pending_order، refund_order) است و تست‌هایش در payment/tests_refund.py.
"""
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']
@admin.action(description='لغوِ سفارشِ در انتظار')
def cancel_pending(self, request, queryset):
from .services import OrderStateError, cancel_pending_order
done = 0
for order in queryset:
try:
cancel_pending_order(order, staff=request.user.get_username())
done += 1
except OrderStateError as exc:
self.message_user(request, str(exc), messages.WARNING)
if done:
self.message_user(request, f'{done} سفارش لغو شد.', messages.SUCCESS)
@admin.action(description='بازپرداخت (پس گرفتنِ اعتبار/اشتراکِ سفارشِ پرداخت‌شده)')
def refund(self, request, queryset):
from django.template.response import TemplateResponse
from .services import OrderStateError, refund_order
if request.POST.get('confirm') != 'yes':
return TemplateResponse(request, 'admin/payment/order/refund_confirm.html', {
**self.admin_site.each_context(request), 'orders': list(queryset), 'opts': self.model._meta,
'title': 'تأییدِ بازپرداخت',
})
for order in queryset:
try:
r = refund_order(order, staff=request.user.get_username())
except OrderStateError as exc:
self.message_user(request, str(exc), messages.WARNING)
continue
if r['subscription_days']:
what = f'{r["subscription_days"]} روز از اشتراک کم شد'
else:
what = f'{r["credits_taken"]} از {r["credits_granted"]} سوالِ بسته پس گرفته شد'
if r['credits_taken'] < r['credits_granted']:
what += f' (بقیه خرج شده بود)'
if r['hearts_days']:
what += f'، {r["hearts_days"]} روز قلبِ نامحدود کم شد'
self.message_user(request, f'سفارشِ {order.id}: {what}. پول را از پنلِ درگاه برگردانید.', messages.SUCCESS)
@admin.register(Transaction)