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 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) 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') 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 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='بازپرداخت (پس گرفتنِ اعتبار/اشتراکِ سفارشِ پرداخت‌شده)', permissions=['change']) 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): """فقط برای دیدن؛ تراکنش را فقط تأییدِ درگاه و 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): """ اتصالِ یک‌باره به 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 lookup_allowed(self, lookup, value, request=None): # فیلترِ querystring (?refresh_token__startswith=a) با شمردنِ نتیجه توکن را حرف‌به‌حرف لو می‌دهد. if lookup.split('__')[0] in ('refresh_token', 'pending_state'): return False return super().lookup_allowed(lookup, value, request) 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='ساختِ لینکِ اتصال به بازار', permissions=['change']) def make_authorize_link(self, request, queryset): url = bazaar.start_authorization() self.message_user(request, format_html( 'این لینک را صاحبِ حسابِ بازار باز کند و «اجازه» بزند: {0}', url, ), messages.SUCCESS)