Money review: Bazaar rejection no longer burns the purchase token, read-only order admin, pack grant from order snapshot

- Bazaar verify: a rejected token is not stored on the order or the failed transaction, so the
  same purchase can still pay the right order; a refunded order refuses a new token (409).
- OrderAdmin: every field read-only, no add/delete; Transaction admin read-only.
- grant_pack uses the order's snapshot when the plan was deleted, logs ERROR when nothing is granted.
- Pay link: authority is stored only if the order amount did not change during PaymentRequest.
- PlanAdmin accepts only IRT (the app always renders toman).
- reconcile_pending_order logs unexpected exceptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 09:26:26 +03:30
parent eb4e97753b
commit fedd10ff22
7 changed files with 268 additions and 22 deletions

View File

@@ -73,17 +73,24 @@ def grant_pack(order) -> bool:
from .models import Plan
plan = Plan.objects.filter(pk=order.plan_id, kind='pack').first()
if plan is None:
plan = Plan.objects.filter(pk=order.plan_id).first()
if plan is not None and plan.kind != 'pack':
logger.error('grant_pack: پلنِ %s سفارشِ %s بسته نیست؛ چیزی داده نشد', order.plan_id, order.id)
return False
# عددها از خودِ سفارش (همان که فروخته شد)، پس پلنِ پاک‌شده جلوی دادنِ آن‌ها را نمی‌گیرد؛ تا امروز
# پرداختِ سفارشی که پلنش پاک شده بود «paid» می‌شد بی‌هیچ اعتبار و بی‌هیچ لاگی (بازتولید شد).
# سفارشِ بی‌این ستون‌ها (پیش از payment.0011) عددهای فعلیِ پلن را می‌گیرد.
if order.question_credits is None and plan is None:
logger.error('grant_pack: سفارشِ %s نه عددِ ثبت‌شده دارد نه پلنِ %s؛ پرداخت شده ولی چیزی داده نشد',
order.id, order.plan_id)
return False
# عددها از خودِ سفارش (همان که فروخته شد)؛ سفارشِ بی‌این ستون‌ها (پیش از آن‌ها، یا ساخته‌شده بیرون
# از create_order) عددهای فعلیِ پلن را می‌گیرد، همان رفتارِ پیشین.
credits = order.question_credits if order.question_credits is not None else plan.question_credits
hearts_days = order.hearts_days if order.hearts_days is not None else plan.hearts_days
hearts_days = order.hearts_days if order.hearts_days is not None else (plan.hearts_days if plan else 0)
note = (order.plan_title or (plan.title if plan else ''))[:100]
try:
with transaction.atomic():
QuestionCredit.objects.create(user_id=order.user_id, delta=credits,
reason='purchase', order_id=order.id, note=plan.title[:100])
reason='purchase', order_id=order.id, note=note)
if hearts_days:
now = timezone.now()
hp = HeartsPass.objects.select_for_update().filter(user_id=order.user_id).first()
@@ -91,6 +98,8 @@ def grant_pack(order) -> bool:
until = start + timezone.timedelta(days=hearts_days)
HeartsPass.objects.update_or_create(user_id=order.user_id, defaults={'expires_at': until})
except IntegrityError:
# اعتبارِ این سفارش از قبل داده شده؛ apply_order_payment_result نباید دو بار «تازه paid» بگوید.
logger.error('grant_pack: اعتبارِ سفارشِ %s از قبل داده شده بود؛ دوباره داده نشد', order.id)
return False
return True