Grades 4 and 5, Bazaar purchase from the app, pack SKUs

- rag: MATH_4 / MATH_5 textbooks (TOC and page offsets read from the PDFs)
- content: grades 4 and 5 in the seed files: 7 units / 28 nodes each,
  204 + 194 questions (every node >= 4), all re-solved blind once
- subscription: Plan.bazaar_sku (admin); POST /api/subscription/purchase
  accepts paymentMethod=bazaar for packs, rejects unknown methods (was ignored)
- payment: idempotency key cannot switch provider; discount codes refused
  on Bazaar orders (Bazaar charges the fixed SKU price)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 20:01:44 +03:30
parent ab5e53730a
commit 8cb326ba94
15 changed files with 16699 additions and 3897 deletions

View File

@@ -25,7 +25,8 @@ class IdempotencyKeyConflict(APIException):
def _ensure_idempotent_match(order: Order, *, user_id: int, amount: int, currency: str,
subscription_id: int | None, plan_id: int | None = None) -> None:
subscription_id: int | None, plan_id: int | None = None,
provider: str | None = None) -> None:
if order.user_id != user_id:
raise IdempotencyKeyConflict(
'این Idempotency-Key متعلق به کاربر دیگری است و نمی‌تواند استفاده شود.'
@@ -35,7 +36,8 @@ def _ensure_idempotent_match(order: Order, *, user_id: int, amount: int, currenc
discounted = order.amount < amount and order.status == 'pending'
if ((order.amount != amount and not discounted) or order.currency != currency
or order.subscription_id != subscription_id
or (plan_id is not None and order.plan_id not in (None, plan_id))):
or (plan_id is not None and order.plan_id not in (None, plan_id))
or (provider is not None and order.provider != provider)):
raise IdempotencyKeyConflict(
'این Idempotency-Key قبلاً با پارامترهای متفاوتی استفاده شده است.'
)
@@ -70,7 +72,7 @@ def create_order(*, user_id: int, amount: int, currency: str = 'IRT',
if existing is not None:
_ensure_idempotent_match(
existing, user_id=user_id, amount=amount, currency=currency,
subscription_id=subscription_id, plan_id=plan_id,
subscription_id=subscription_id, plan_id=plan_id, provider=provider,
)
payment_url = None if existing.provider == 'bazaar' else call_payment_gateway(existing)
return existing, payment_url
@@ -97,7 +99,7 @@ def create_order(*, user_id: int, amount: int, currency: str = 'IRT',
raise
_ensure_idempotent_match(
order, user_id=user_id, amount=amount, currency=currency,
subscription_id=subscription_id, plan_id=plan_id,
subscription_id=subscription_id, plan_id=plan_id, provider=provider,
)
# برای provider='bazaar' هیچ paymentUrlی وجود ندارد: کلاینت اندروید مستقیماً