158 lines
7.3 KiB
Python
158 lines
7.3 KiB
Python
from django.contrib import admin, messages
|
||
from django.utils.html import format_html
|
||
|
||
from . import bazaar
|
||
from .models import BazaarCredential, Order, PaymentEvent, 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 را ۱۰۰۰ کرد و با ۱۰۰۰ تومان خرید (بازتولید شد)
|
||
# (بازتولید شد). سفارش سندِ مالی است: از پنل ساخته یا پاک هم نمیشود؛ پاک شدنِ سفارشِ پرداختشده
|
||
# تراکنشهایش را میبرد و توکنِ خریدش برای سفارشِ دیگری دوباره پذیرفته میشد.
|
||
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
|
||
what = (f'{r["subscription_days"]} روز از اشتراک کم شد' if r['subscription_days']
|
||
else 'اشتراکی برای کم کردن نبود')
|
||
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(PaymentEvent)
|
||
class PaymentEventAdmin(admin.ModelAdmin):
|
||
"""دفترِ رویدادهای مسیرِ خرید، فقط برای دیدن؛ ردیفها را فقط `payment/events.py` مینویسد
|
||
(دفترِ افزودنی؛ ویرایش یا حذفِ دستی آن را بیاعتبار میکرد)."""
|
||
|
||
list_display = ('created_at', 'event', 'provider', 'order_id', 'user_id')
|
||
list_filter = ('event', 'provider', 'created_at')
|
||
search_fields = ('order_id', 'user_id')
|
||
readonly_fields = ('order_id', 'user_id', 'provider', 'event', 'detail', 'created_at')
|
||
|
||
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(
|
||
'این لینک را صاحبِ حسابِ بازار باز کند و «اجازه» بزند: <a href="{0}">{0}</a>', url,
|
||
), messages.SUCCESS)
|