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>
106 lines
4.7 KiB
Python
106 lines
4.7 KiB
Python
from django.contrib import admin, messages
|
||
from django.utils.html import format_html
|
||
|
||
from . import bazaar
|
||
from .models import BazaarCredential, Order, Transaction
|
||
|
||
|
||
class TransactionInline(admin.TabularInline):
|
||
model = Transaction
|
||
extra = 0
|
||
|
||
|
||
@admin.register(Order)
|
||
class OrderAdmin(admin.ModelAdmin):
|
||
"""
|
||
لغو و بازپرداخت فقط با دو 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)
|
||
class TransactionAdmin(admin.ModelAdmin):
|
||
list_display = ('id', 'order', 'gateway', 'status', 'paid_at')
|
||
list_filter = ('gateway', 'status')
|
||
|
||
|
||
@admin.register(BazaarCredential)
|
||
class BazaarCredentialAdmin(admin.ModelAdmin):
|
||
"""
|
||
اتصالِ یکباره به API بازار، بدونِ SSH: action لینکِ «اجازه» را میسازد؛
|
||
صاحبِ حسابِ بازار بازش میکند و بازار به verify برمیگردد. خودِ توکن
|
||
هیچجا نمایش داده نمیشود.
|
||
"""
|
||
|
||
list_display = ('__str__', 'authorized_at', 'pending_since')
|
||
fields = ('connected', 'authorized_at', 'pending_since')
|
||
readonly_fields = fields
|
||
actions = ['make_authorize_link']
|
||
|
||
@admin.display(boolean=True, description='توکن دارد')
|
||
def connected(self, obj):
|
||
return bool(obj.refresh_token)
|
||
|
||
def changelist_view(self, request, extra_context=None):
|
||
BazaarCredential.objects.get_or_create(pk=1)
|
||
return super().changelist_view(request, extra_context)
|
||
|
||
def has_add_permission(self, request):
|
||
return False
|
||
|
||
@admin.action(description='ساختِ لینکِ اتصال به بازار')
|
||
def make_authorize_link(self, request, queryset):
|
||
url = bazaar.start_authorization()
|
||
self.message_user(request, format_html(
|
||
'این لینک را صاحبِ حسابِ بازار باز کند و «اجازه» بزند: <a href="{0}">{0}</a>', url,
|
||
), messages.SUCCESS)
|