Files
MathApp/payment/admin.py
Zareian 947f946909 Question packs removed at the root; all subscriptions 4 assistant questions a day; Bazaar is subscription-only
- subscription.0017/0018, payment.0013: pack plans deleted, QuestionCredit/HeartsPass models and Plan.kind /
  question_credits / hearts_days / Order.question_credits / hearts_days dropped. The only paid pack order was the
  company owner's test account (user 26).
- Assistant: no credit spending; quota is the daily cap only (free 2, every plan 4, gift subscription 4).
- Bazaar: verify_purchase is subscription-only (no inapp guess), start/purchase responses lose productType,
  verify refuses an order without a subscription. A paid order with no subscription now logs ERROR.
- Removed POST /api/payment/orders (internal; no caller). Its permission tests stay.
- Pack-based tests rewritten on subscriptions (refund, security, Bazaar token); pack-only test files deleted.
- docs: profit table for 4/day on both payment routes plus free users; handbook, operations, CLAUDE.md current.
949 tests green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:33:38 +03:30

138 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 را ۱۰۰۰ کرد و با ۱۰۰۰ تومان خرید (بازتولید شد)
# (بازتولید شد). سفارش سندِ مالی است: از پنل ساخته یا پاک هم نمی‌شود؛ پاک شدنِ سفارشِ پرداخت‌شده
# تراکنش‌هایش را می‌برد و توکنِ خریدش برای سفارشِ دیگری دوباره پذیرفته می‌شد.
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(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)