Bazaar: subscriptions (10343/10344), dynamic price (discount codes on Bazaar), auto-renewal sync

- Purchase with paymentMethod=bazaar now works for subscription plans with a bazaar_sku (not only packs);
  response carries bazaarProductType. Migration subscription.0014 puts 10343/10344 on the 90/180-day plans.
- Verify asks Bazaar for the right product type (from the order) and stores validUntil (payment.0012).
- POST /api/payment/bazaar/orders/{id}/start: SKU, type, and an HS256 dynamic-price JWT when a discount
  code lowered the price (BAZAAR_DYNAMIC_PRICE_KEY); discount on Bazaar orders allowed only with that key
  and only before start.
- Auto-renewal: sync_bazaar_renewals re-asks Bazaar when the paid period is near its end (status page and
  the paywall via has_subscription_access); each new period becomes a paid order.
- Default BAZAAR_PACKAGE_NAME = com.tadrisino.math.app; health shows bazaarDynamicPrice.
979 tests green. No real Bazaar purchase or renewal tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 17:53:11 +03:30
parent 2e0753587c
commit 8077ab4350
20 changed files with 580 additions and 40 deletions

View File

@@ -43,7 +43,7 @@ class Plan(models.Model):
default=0, help_text='فقط بسته: چند روز قلبِ نامحدود هدیه.')
# پلنی که دیگر فروخته نمی‌شود ولی مشترک‌های فعلی‌اش هنوز به آن وصل‌اند.
on_sale = models.BooleanField(default=True, help_text='در صفحه‌ی خرید نشان داده شود.')
# شناسه‌ی همین بسته در پیشخانِ کافه‌بازار (محصولِ درون‌برنامه‌ای، مصرف‌شدنی). خریدِ بازار
# شناسه‌ی همین پلن در پیشخانِ کافه‌بازار: بسته = محصولِ مصرف‌شدنی، اشتراک = محصولِ «اشتراکی». خریدِ بازار
# SKU را فقط از اینجا می‌گیرد، هرگز از کلاینت: وگرنه توکنِ ارزان‌ترین محصول بسته‌ی گران را
# فعال می‌کرد. خالی یعنی این بسته در بازار فروخته نمی‌شود (درخواستش ۴۰۰ می‌گیرد).
bazaar_sku = models.CharField(