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>
This commit is contained in:
@@ -35,46 +35,38 @@ Message.objects.filter(role='assistant', created_at__gte=since).values('conversa
|
||||
|
||||
## اشتراکها و سقفِ روزانهی دستیار
|
||||
|
||||
فقط اشتراک فروخته میشود (`subscription.0016`، تصمیمِ سینیور). قیمتها همان قیمتِ پیشخانِ بازار است و کاربر در هر دو
|
||||
راهِ پرداخت همین را میبیند. بستههای سوال `on_sale=False` هستند؛ اعتبارِ کسی که قبلاً خریده (کاربرِ ۲۶، ۲۰ سوال)
|
||||
همچنان خرج میشود.
|
||||
فقط اشتراک فروخته میشود؛ بستهی سوال از ریشه حذف شد (`subscription.0016`، `0017`، `0018`). قیمتها همان قیمتِ پیشخانِ
|
||||
بازار است (به گفتهی سینیور) و هر سه **۴ سوال در روز** از دستیار دارند (`Plan.ai_daily_quota`، از ادمین عوضشدنی).
|
||||
|
||||
**فرضها:** هر سوال ~۷۰۰ تومان (دستِ بالا، بخشِ بالا). از هر خریدِ بازار اول ۱۰٪ ارزشِ افزوده کم میشود، از باقی ۸۵٪
|
||||
سهمِ ماست (زیرِ یک میلیارد تومان فروشِ سالانه؛ بالاترش ۷۰٪)، و ۱۲۰ تومان کارمزد (سندِ «محاسبه سهم توسعهدهنده در
|
||||
بازار»). یعنی **~۷۷٪ِ قیمت** میرسد. زرینپال بیسهمِ بازار است (~۹۰٪، کارمزدش را نسنجیدهایم)، پس بازار بدترین حالت است.
|
||||
**فرضها:**
|
||||
- هر سوالِ دستیار ~۷۰۰ تومان (دستِ بالا؛ بخشِ بالا).
|
||||
- **بازار:** ۱۰٪ ارزشِ افزوده از قیمت کم میشود، از باقی ۸۵٪ سهمِ ماست (زیرِ یک میلیارد تومان فروشِ سالانه؛ بالاترش
|
||||
۷۰٪)، و کارمزدِ تراکنش (سندِ «محاسبه سهم توسعهدهنده در بازار»). یعنی ~۷۷٪ِ قیمت میرسد.
|
||||
- **زرینپال:** همان ۱۰٪ ارزشِ افزوده و ~۱٪ کارمزد. کارمزدِ واقعیِ قراردادِ زرینپالِ شرکت را **نسنجیدهایم**.
|
||||
- بدترین حالت: مشترک هر روزِ دوره کلِ ۴ سوال را بپرسد. هزینهی سرور حساب نشده (ثابت است، نه به ازای کاربر).
|
||||
|
||||
| اشتراک | قیمت (تومان) | خالصِ بازار در روز | سقفِ دستیار | هزینهی دستیار اگر هر روز کلِ سقف را بپرسد |
|
||||
|---|---|---|---|---|
|
||||
| یکماهه | ۵۹۸٬۰۰۰ | ~۱۵٬۴۰۰ | ۵ در روز | ۳٬۵۰۰ در روز، ۲۳٪ |
|
||||
| سهماهه | ۸۹۸٬۰۰۰ | ~۷٬۷۰۰ | ۳ در روز | ۲٬۱۰۰ در روز، ۲۷٪ |
|
||||
| ششماهه | ۱٬۳۹۸٬۰۰۰ | ~۶٬۰۰۰ | ۳ در روز | ۲٬۱۰۰ در روز، ۳۵٪ |
|
||||
| اشتراک | قیمت (تومان) | راهِ پرداخت | به ما میرسد | هزینهی دستیار در بدترین حالت | سهمِ دستیار | سودِ بدترین حالت |
|
||||
|---|---|---|---|---|---|---|
|
||||
| یکماهه | ۵۹۸٬۰۰۰ | بازار | ۴۶۲٬۰۰۰ | ۸۴٬۰۰۰ | ۱۸٪ | ۳۷۸٬۰۰۰ |
|
||||
| یکماهه | ۵۹۸٬۰۰۰ | زرینپال | ۵۳۸٬۰۰۰ | ۸۴٬۰۰۰ | ۱۶٪ | ۴۵۴٬۰۰۰ |
|
||||
| سهماهه | ۸۹۸٬۰۰۰ | بازار | ۶۹۴٬۰۰۰ | ۲۵۲٬۰۰۰ | ۳۶٪ | ۴۴۲٬۰۰۰ |
|
||||
| سهماهه | ۸۹۸٬۰۰۰ | زرینپال | ۸۰۸٬۰۰۰ | ۲۵۲٬۰۰۰ | ۳۱٪ | ۵۵۶٬۰۰۰ |
|
||||
| ششماهه | ۱٬۳۹۸٬۰۰۰ | بازار | ۱٬۰۸۰٬۰۰۰ | ۵۰۴٬۰۰۰ | ۴۷٪ | ۵۷۶٬۰۰۰ |
|
||||
| ششماهه | ۱٬۳۹۸٬۰۰۰ | زرینپال | ۱٬۲۵۸٬۰۰۰ | ۵۰۴٬۰۰۰ | ۴۰٪ | ۷۵۴٬۰۰۰ |
|
||||
|
||||
پس حتی اگر هر مشترک هر روز تا آخرِ سقف بپرسد، دستِکم ۶۵٪ِ درآمدِ بازار میماند (هزینهی سرور جدا). مصرفِ واقعی کمتر
|
||||
است: روی سرور (فقط دمو و آزمونگرها، ۲۲ روزـکاربر) میانهی روزهایی که کسی پرسید ۳ سوال بود، و همه هر روز نمیآیند؛ با
|
||||
کاربرِ واقعی دوباره بسنجید (پرسوجوی بالا). هر ردیف با دو برابر شدنِ سقف تقریباً دو برابر میشود: ششماهه با ۵ در روز ۵۸٪.
|
||||
سقف در ادمین (`Plan.ai_daily_quota`) عوض میشود و کارتِ خرید آن را از همان عدد نشان میدهد.
|
||||
**کاربرِ رایگان:** فصلِ اولِ هر کتاب و `AI_DAILY_QUOTA_FREE` (۲) سوال در روز. درآمد ندارد، پس هزینهاش خالص است: ۱٬۴۰۰
|
||||
تومان برای هر روزی که میآید و هر دو سوال را میپرسد (۴۲٬۰۰۰ تومان اگر یک ماه هر روز بیاید). سقفِ کلِ سامانه
|
||||
(`AI_GLOBAL_DAILY_MAX`، ۳٬۰۰۰ سوال در روز) بیشترین خرجِ روزانهی دستیار را ~۲٫۱ میلیون تومان نگه میدارد.
|
||||
|
||||
- **ترتیبِ خرج:** اول سقفِ روزانه (بیاشتراک `AI_DAILY_QUOTA_FREE`، پیشفرض ۲؛ مشترک `Plan.ai_daily_quota`)، بعد
|
||||
اعتبارِ بستهی قدیمی اگر مانده. سوالِ رایگانِ بیاشتراکها هزینهی خالص است: ۲ × ۷۰۰ = ۱٬۴۰۰ تومان برای هر کاربرِ
|
||||
فعال در روز.
|
||||
مصرفِ واقعی کمتر از بدترین حالت است چون همه هر روز نمیآیند؛ روی سرور (فقط دمو و آزمونگرها) میانهی روزهایی که کسی
|
||||
پرسید ۳ سوال بود. با کاربرِ واقعی دوباره بسنجید (پرسوجوی بالا). سهمِ دستیار با سقف خطی بالا میرود: ششماهه با ۳ در روز
|
||||
۳۵٪ و با ۵ در روز ۵۸٪ِ درآمدِ بازار است.
|
||||
|
||||
- **ترتیبِ خرج:** فقط سقفِ روزانه (بیاشتراک `AI_DAILY_QUOTA_FREE`؛ مشترک `Plan.ai_daily_quota`؛ اشتراکِ هدیهی بیپلن
|
||||
`DEFAULT_SUBSCRIBER_AI_QUOTA` = ۴). سوالِ بیشتر فروخته نمیشود.
|
||||
- **چه چیزی خرج میشود:** هر سوالی که به API برسد، از جمله ردِ «غیرِ ریاضی» توسطِ مدل. ردِ صافیِ سرور،
|
||||
جوابِ کششده و جوابی که ساخته نشد چیزی کم نمیکنند. `Message.charge` روی هر پیامِ کاربر میگوید
|
||||
سوال از کجا پرداخت شد (`free` / `credit` / `none`).
|
||||
- **دفترِ اعتبار** `subscription.QuestionCredit`: هر خرید یک ردیفِ مثبت دارد (هر سفارش فقط یک بار، با
|
||||
قیدِ یکتا) و هر سوال یک ردیفِ ۱−. کم شدن داخلِ همان تراکنشی است که ردیفِ کاربر را قفل میکند. دو
|
||||
سوالِ همزمان با یک اعتبار، و یک Idempotency-Key که همزمان دو بار برسد، از ۲۰۲۶-۱۰-۰۲ با دو thread
|
||||
واقعی آزموده میشوند (`ai_tutor/tests_credit_concurrency.py`): یک جواب و یک اعتبار. بدونِ
|
||||
`transaction_mode: IMMEDIATE`ِ SQLite همین تست ۵۰۰ میگیرد. روی Postgres سنجیده نشده.
|
||||
- **قلبِ نامحدودِ بسته** در `HeartsPass` است، جدا از `Subscription`، تا خریدِ بسته اشتراکِ کسی را عوض
|
||||
نکند. از پایانِ کارتِ قبلی ادامه پیدا میکند. `HEARTS_ENFORCED` حالا پیشفرض روشن است، پس قلبِ صفر
|
||||
واقعاً جلوی جواب را میگیرد، و پایانِ درس یا جزیرهی بیجواب را هم (`content/tests_hearts_gate.py`).
|
||||
- **لغو و بازپرداخت** از پنلِ ادمین (`/admin/payment/order/`): بازپرداخت فقط اعتبارِ خرجنشدهی همان
|
||||
بسته و روزهای قلبش را پس میگیرد؛ پول از پنلِ درگاه برگردانده میشود. روند در `docs/operations.md`.
|
||||
- اپ موجودی را از `GET /api/ai/quota` میخواند و بعد از هر جواب از `remainingQuota` و `credits`ِ خودِ
|
||||
جواب. صفحهی خرید `GET /api/subscription/status` (`questionCredits`، `unlimitedHeartsUntil`) را میخواند.
|
||||
- **کافهبازار:** هر بسته باید در پنلِ بازار «محصولِ مصرفی» تعریف شود و اپِ اندروید بعد از تأیید
|
||||
consume کند. این هنوز انجام نشده است. سفارشِ بازارِ یک بسته باید با `planId` ساخته شود
|
||||
(`POST /api/payment/orders`)؛ تا ۲۰۲۶-۱۰-۰۲ این فیلد وجود نداشت و پرداختِ بسته از بازار هیچ اعتباری نمیداد.
|
||||
جوابِ کششده و جوابی که ساخته نشد چیزی کم نمیکنند. `Message.charge` روی هر پیامِ کاربر میگوید سوال از سهمیه بود
|
||||
(`free`) یا نه (`none`).
|
||||
|
||||
## محافظِ دستیار (`rag/services/guardrails.py`)
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ temperature و با `OPENAI_CHAT_REASONING_EFFORT` صدا زده میشوند
|
||||
|
||||
## قلبها، مثلِ دولینگو
|
||||
|
||||
هر پاسخِ غلط یک قلب کم میکند (اشتراکِ فعال یا روزهای قلبِ بستهی سوال = قلبِ نامحدود). **هر ۵ ساعت یک
|
||||
هر پاسخِ غلط یک قلب کم میکند (اشتراکِ فعال = قلبِ نامحدود). **هر ۵ ساعت یک
|
||||
قلب برمیگردد** (`HEART_REFILL_INTERVAL`) و **۲۴ ساعت بعد از اولین قلبی که از
|
||||
حالتِ پر کم شد، همه یکجا پر میشوند** (`HEART_FULL_REFILL_AFTER`، فیلدِ
|
||||
`full_refill_at`). `GET /api/user/hearts` هر دو زمان را میدهد: `nextRefillAt`
|
||||
|
||||
@@ -68,5 +68,5 @@
|
||||
این تنها عددِ قابلِ اتکاست، چون `hard_limit_usd` موجودی نیست.
|
||||
|
||||
۳. **ردیفِ تازه اضافه کنید**، با تاریخ، مدل، عدد و مدرک. ردیفِ قدیمی را نگه دارید. اگر قیمتِ
|
||||
دستیار عوض شد، قیمتِ بستهها (`subscription.0010`) را هم دوباره بسنجید: سهمِ هزینه از درآمدِ هر
|
||||
بسته در `docs/ai-cost-and-guardrails.md` با همین عددها حساب شده است.
|
||||
دستیار عوض شد، سقفِ روزانهی اشتراکها (`Plan.ai_daily_quota`) را هم دوباره بسنجید: سهمِ هزینه از درآمدِ هر
|
||||
اشتراک در `docs/ai-cost-and-guardrails.md` با همین عددها حساب شده است.
|
||||
|
||||
@@ -267,14 +267,14 @@ callbackِ زرینپال، برگشت از پرداخت، `BAZAAR_REDIRECT_U
|
||||
- **«لغوِ سفارشِ در انتظار»**: برای سفارشی که هنوز پرداخت نشده. پولی جابهجا نشده است. اگر کاربر
|
||||
همان لحظه در درگاه پرداخت کرده باشد، تأییدِ زرینپال سفارش را دوباره «پرداختشده» میکند و او چیزش را میگیرد.
|
||||
- **«بازپرداخت»**: برای سفارشِ پرداختشده. اول یک صفحهی تأیید میآید. بعد از تأیید، سفارش
|
||||
«بازپرداختشده» میشود و هر چه داده بود تا جایی که هنوز مانده پس گرفته میشود: سوالهای
|
||||
خرجنشدهی همان بسته (موجودی منفی نمیشود)، روزهای قلبِ نامحدودِ همان بسته، یا روزهای همان اشتراک.
|
||||
«بازپرداختشده» میشود و روزهای اشتراکِ همان سفارش از پایانِ اشتراک کم میشود (اگر چیزی نماند، اشتراک
|
||||
«لغوشده» میشود).
|
||||
پورسانتِ معرف هم برمیگردد. پیامِ بالای صفحه میگوید دقیقاً چه چیزی پس گرفته شد.
|
||||
3. **خودِ پول را این دکمه برنمیگرداند.** پول را در پنلِ زرینپال برگردانید. خریدِ کافهبازار هم از
|
||||
سمتِ خودِ بازار برگردانده میشود.
|
||||
|
||||
وضعیتِ سفارش در فرمِ ادمین فقطخواندنی است. اگر وضعیت را با دست از «پرداختشده» به «ناموفق» عوض
|
||||
میکردید، اعتبار و اشتراکی که سفارش داده بود سرِ جایش میماند. کاربر بازپرداخت را در صفحهی
|
||||
میکردید، اشتراکی که سفارش داده بود سرِ جایش میماند. کاربر بازپرداخت را در صفحهی
|
||||
«پرداختها و فاکتورها» میبیند. کد در `payment/services.py` (`refund_order`، `cancel_pending_order`)
|
||||
است و تستهایش در `payment/tests_refund.py`.
|
||||
|
||||
@@ -283,7 +283,7 @@ callbackِ زرینپال، برگشت از پرداخت، `BAZAAR_REDIRECT_U
|
||||
اپِ اندروید (پوستهی WebViewِ همین سایت، `android/` در ریپوی فرانت) با پرداختِ درونبرنامهایِ بازار میفروشد. در
|
||||
برگهی پرداخت دو کاشی همیشه کنارِ هماند و کاربر هر کدام را بخواهد میزند: «کارتِ بانکی» (زرینپال، پیشفرض) و
|
||||
«کافهبازار». در مرورگر، یا وقتی پلن SKU ندارد، کاشیِ بازار غیرفعال است و دلیلش را میگوید. فقط اشتراک فروخته میشود
|
||||
(بستهها `on_sale=False`، `subscription.0016`)؛ مسیرِ بسته (`inapp`، consume) برای روزی که ادمین دوباره روشنش کند مانده. داخلِ اپ
|
||||
(بستهی سوال از ریشه حذف شد، `subscription.0017`/`0018`). داخلِ اپ
|
||||
زیرِ «کافهبازار» قیمتی میآید که خودِ بازار برای آن SKU دارد (Poolakey روی گوشی؛ API سرور قیمت نمیدهد).
|
||||
|
||||
تفاوتِ روشها در یک جاست: `payment/providers.py` (هر روش یک کلاس، `provider_for`/`provider_for_method` انتخابش
|
||||
@@ -291,7 +291,7 @@ callbackِ زرینپال، برگشت از پرداخت، `BAZAAR_REDIRECT_U
|
||||
|
||||
| چه | کجا | الان |
|
||||
|---|---|---|
|
||||
| محصول در پیشخانِ بازار | اشتراک = محصولِ «اشتراکی» (بازار خودکار تمدید میکند)، بسته = «مصرفشدنی» | یکماهه `10342`، سهماهه `10343`، ششماهه `10344` (تأییدِ سینیور) |
|
||||
| محصول در پیشخانِ بازار | محصولِ «اشتراکی» (بازار خودکار تمدید میکند) | یکماهه `10342`، سهماهه `10343`، ششماهه `10344` (تأییدِ سینیور) |
|
||||
| SKU روی پلن | `/admin/subscription/plan/` ← «Bazaar sku» (سرور SKU را فقط از اینجا میگیرد) | سه اشتراکِ بالا (`subscription.0014`، `0015`) |
|
||||
| قیمت | بازار قیمتِ پیشخان را میگیرد؛ ادمین باید با آن یکی باشد | ۵۹۸/۸۹۸/۱٬۳۹۸ هزار تومان در هر دو (`subscription.0016`، به گفتهی سینیور)؛ داخلِ اپ قیمتِ بازار روی کاشی دیده میشود |
|
||||
| کلیدِ «تخفیف پویا» | `BAZAAR_DYNAMIC_PRICE_KEY` در `.env` سرور | با آن کدِ تخفیف روی خریدِ بازار هم کار میکند |
|
||||
@@ -299,13 +299,12 @@ callbackِ زرینپال، برگشت از پرداخت، `BAZAAR_REDIRECT_U
|
||||
|
||||
مسیر:
|
||||
|
||||
1. `POST /api/subscription/purchase` با `paymentMethod: "bazaar"` → سفارش، `bazaarSku`، `bazaarProductType`.
|
||||
1. `POST /api/subscription/purchase` با `paymentMethod: "bazaar"` → سفارش و `bazaarSku`.
|
||||
2. (اختیاری) `POST /api/discount/redeem` روی همان سفارش. فقط با کلیدِ «تخفیف پویا»، وگرنه ۴۰۰.
|
||||
3. `POST /api/payment/bazaar/orders/{id}/start` → SKU، نوع، و اگر تخفیف خورده `dynamicPriceToken` (JWTِ HS256 با مبلغ
|
||||
3. `POST /api/payment/bazaar/orders/{id}/start` → SKU، و اگر تخفیف خورده `dynamicPriceToken` (JWTِ HS256 با مبلغ
|
||||
به ریال). بیتخفیف توکن نمیرود چون بازارِ قدیمیتر از ۱۳.۳ با آن خطا میدهد. از این لحظه کدِ تخفیف نمینشیند.
|
||||
4. اپ با Poolakey میخرد (`subscribeProduct` یا `purchaseProduct`، `payload` = شمارهی سفارش).
|
||||
5. `POST /api/payment/bazaar/verify` توکن را از خودِ بازار میسنجد (نوعِ محصول از سفارش) و اشتراک را فعال یا
|
||||
اعتبار را اضافه میکند. فقط بسته بعد از آن consume میشود.
|
||||
4. اپ با Poolakey اشتراک میخرد (`subscribeProduct`، `payload` = شمارهی سفارش).
|
||||
5. `POST /api/payment/bazaar/verify` توکن را از خودِ بازار میسنجد و اشتراک را فعال میکند. اشتراک consume نمیشود.
|
||||
|
||||
**تمدیدِ خودکار:** بازار به ما خبر نمیدهد. وقتی دورهی پرداختشدهی کاربر (`Order.bazaar_valid_until`) کمتر از یک روز
|
||||
مانده یا گذشته، با باز شدنِ وضعیتِ اشتراک (`GET /api/subscription/status`) همان توکن از بازار پرسیده میشود (هر توکن
|
||||
@@ -313,7 +312,7 @@ callbackِ زرینپال، برگشت از پرداخت، `BAZAAR_REDIRECT_U
|
||||
(`payment.services.sync_bazaar_renewals`). مبلغِ آن سفارش قیمتِ فعلیِ ادمین است، چون بازار مبلغِ تمدید را نمیگوید.
|
||||
اگر بازار در دسترس نباشد، کاربرِ تمدیدشده تا تلاشِ ساعتِ بعد پشتِ دیوار میماند. لغوِ تمدید از خودِ بازار است.
|
||||
|
||||
تستها: `payment/tests_bazaar_subscription.py`، `payment/tests_bazaar_pack.py`، `payment/tests_bazaar_oauth.py`.
|
||||
تستها: `payment/tests_bazaar_subscription.py`، `payment/tests_bazaar_oauth.py`.
|
||||
**هیچ خریدِ واقعیای از بازار انجام نشده**؛ نه پذیرفتنِ JWT از سوی بازار و نه تمدیدِ واقعی.
|
||||
|
||||
## ورودِ توسعهدهندهها (بدونِ سرویسِ پیامک)
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
| `progress` | امتیاز، سکه، قلب، زنجیره، جدولِ رتبه، همراهها | `UserStats UserHearts StreakRecord ActiveDay LeaderboardEntry Character` |
|
||||
| `ai_tutor` | گفتگو با دستیار، سهمیه، بازخوردِ 👍/👎 | `Conversation Message MessageFeedback AskIdempotency` |
|
||||
| `rag` | خواندنِ کتاب، Qdrant، ساختِ پرامپت، شکلها | `LessonChunk LessonChunkImage IngestedPage` |
|
||||
| `subscription` | پلنها (بستهی سوال، اشتراکِ قدیمی)، دفترِ اعتبارِ سوال، قلبِ نامحدودِ بسته | `Plan Subscription QuestionCredit HeartsPass` |
|
||||
| `subscription` | پلنهای اشتراک و اشتراکِ هر کاربر | `Plan Subscription` |
|
||||
| `payment` | سفارش، تراکنش، زرینپال، بازار، لغو و بازپرداخت از ادمین | `Order Transaction BazaarCredential` |
|
||||
| `discount` | کدِ تخفیف و پیشنهادها | `DiscountCode Redemption Offer UserOffer` |
|
||||
| `affiliate` | معرفی به دوستان و پورسانت | `AffiliateAccount Conversion` |
|
||||
@@ -314,24 +314,24 @@ with transaction.atomic():
|
||||
|
||||
سه نکتهی طراحی: (۱) تماس با GapGPT **بیرون از تراکنشِ دیتابیس** است، وگرنه ۳۰ ثانیه انتظار دیتابیس را قفل میکرد؛ (۲) اگر GapGPT خطا داد، سهمیهی رزروشده پس داده میشود و کاربر ۵۰۳ میگیرد، نه ۵۰۰؛ (۳) اولین پیامِ هر گفتگو کش میشود — سؤالِ تکراری از کش ۰٫۰۱ ثانیهای برمیگردد در برابرِ ~۱۱ ثانیه.
|
||||
|
||||
### ۶-۴ خریدِ بستهی سوال با زرینپال
|
||||
### ۶-۴ خریدِ اشتراک (زرینپال یا کافهبازار)
|
||||
|
||||

|
||||

|
||||
|
||||
*کد: `subscription/views.py` · `payment/services.py` · `subscription/services.py → grant_pack` · `payment/views.py` ( `PaymentRedirectView`، `ZarinpalCallbackView` ) · `payment/zarinpal.py` · تستها: `payment/tests_audit_fixes.py` · `subscription/tests_packs.py` · `payment/tests_refund.py`*
|
||||
*کد: `subscription/views.py` · `payment/providers.py` · `payment/services.py` · `subscription/services.py → activate_subscription` · `payment/views.py` ( `PaymentRedirectView`، `ZarinpalCallbackView`، `BazaarStartPurchaseView`، `BazaarVerifyPurchaseView` ) · `payment/zarinpal.py` · `payment/bazaar.py` · تستها: `payment/tests_audit_fixes.py` · `payment/tests_bazaar_subscription.py` · `payment/tests_refund.py`*
|
||||
|
||||
از ۱۰ مهر ۱۴۰۵ آنچه فروخته میشود **بستهی سوال** است، نه اشتراکِ ماهانه/سالانه: ۲۰ / ۶۰ / ۱۵۰ سوال از دستیار به ۴۵ / ۱۱۹ / ۲۵۹ هزار تومان، بیانقضا، با ۷ / ۳۰ / ۶۰ روز قلبِ نامحدودِ هدیه. پرداختِ تأییدشده یک ردیفِ مثبت در دفترِ `QuestionCredit` و روزهای `HeartsPass` میسازد و به اشتراکِ کسی دست نمیزند. پلنهای اشتراکیِ قبلی پاک نشدند ( `on_sale=False` ) تا مشترکِ فعلی سقفش را نگه دارد.
|
||||
فقط **اشتراک** فروخته میشود: یک / سه / ششماهه به ۵۹۸ / ۸۹۸ / ۱٬۳۹۸ هزار تومان، هر سه با همهی فصلها، قلبِ نامحدود و ۴ سوال در روز از دستیار. کاربر در برگهی پرداخت بینِ «کارتِ بانکی» (زرینپال، پیشفرض) و «کافهبازار» (فقط داخلِ اپِ اندروید) انتخاب میکند. پرداختِ تأییدشده روزهای همان پلن را از پایانِ اشتراکِ فعلی اضافه میکند. تفاوتِ دو راهِ پرداخت در `payment/providers.py` است (هر راه یک کلاس).
|
||||
|
||||
> #### چرا اینقدر پیچیده؟
|
||||
>
|
||||
> - **ساختِ سفارش به زرینپال وابسته نیست:** اگر زرینپال قطع باشد، سفارش ساخته میشود و فقط رفتن به درگاه عقب میافتد.
|
||||
> - **برگشتِ کاربر هیچ چیزی را ثابت نمیکند:** هر کس میتواند آدرسِ برگشت را با `Status=OK` بسازد. فقط تأییدِ سرور-به-سرور سفارش را «پرداختشده» میکند.
|
||||
> - **پرداختِ جاافتاده بازسنجی میشود:** اگر زرینپال موقعِ برگشت جواب نداد، هر بار که اپ وضعیتِ خرید را میخواند سفارش دوباره سنجیده میشود (و دستورِ `payment_reconcile` ).
|
||||
> - **پلن روی خودِ سفارش است** ؛ کدِ تخفیفِ ۱۰۰٪ بیدرگاه تمام میکند؛ تأییدِ تکراری اعتبارِ دوباره نمیدهد (قیدِ یکتا روی سفارش).
|
||||
> - **کافهبازار:** سفارشِ بازار با `planId` ساخته میشود ( `POST /api/payment/orders` )؛ تا ۱۰ مهر این فیلد نبود و خریدِ بسته از بازار هیچ اعتباری نمیداد (در بازبینی پیدا و با تست بسته شد).
|
||||
> - **لغو و بازپرداخت از پنلِ ادمین:** دو action روی سفارشها؛ بازپرداخت پس از صفحهی تأیید، اعتبارِ خرجنشده و روزهای قلب یا اشتراکِ همان سفارش را پس میگیرد. خودِ پول از پنلِ درگاه برگردانده میشود ( `docs/operations.md` ).
|
||||
> - **پلن و مدت روی خودِ سفارش است** ؛ کدِ تخفیفِ ۱۰۰٪ بیدرگاه تمام میکند؛ تأییدِ تکراری روزِ دوباره نمیدهد (قیدِ یکتا روی تراکنش).
|
||||
> - **کافهبازار:** اپ با Poolakey اشتراک میخرد و سرور توکن را از خودِ بازار میسنجد؛ بازار تمدیدِ خودکار را خبر نمیدهد، پس سرور نزدیکِ پایانِ دوره خودش میپرسد ( `sync_bazaar_renewals` ). کدِ تخفیف با «تخفیف پویا»ی بازار به قیمت میرسد.
|
||||
> - **لغو و بازپرداخت از پنلِ ادمین:** دو action روی سفارشها؛ بازپرداخت پس از صفحهی تأیید روزهای اشتراکِ همان سفارش را پس میگیرد. خودِ پول از پنلِ درگاه برگردانده میشود ( `docs/operations.md` ).
|
||||
>
|
||||
> آزمودهنشده زرینپال درخواستِ پرداخت را با کدِ ۱۰۰ میپذیرد، ولی یک پرداختِ واقعی تا انتها هنوز انجام نشده.
|
||||
> آزمودهنشده زرینپال درخواستِ پرداخت را با کدِ ۱۰۰ میپذیرد، ولی یک پرداختِ واقعی تا انتها هنوز انجام نشده؛ خریدِ بازار منتظرِ ساختِ اپِ اندروید است.
|
||||
|
||||
### ۶-۵ گزارشِ والدین
|
||||
|
||||
@@ -386,11 +386,9 @@ with transaction.atomic():
|
||||
- `IngestedPage collection, page`
|
||||
|
||||
**subscription · payment پول**
|
||||
- `Plan kind (pack/subscription), price, question_credits, hearts_days, on_sale, ai_daily_quota`
|
||||
- `Plan price, duration_days, on_sale, ai_daily_quota, bazaar_sku`
|
||||
- `Subscription user_id, → Plan, status, expires_at`
|
||||
- `QuestionCredit user_id, delta (+خرید / −سوال / −بازپرداخت), order_id, message_id`
|
||||
- `HeartsPass user_id, expires_at قلبِ نامحدودِ بسته`
|
||||
- `Order user_id, plan_id, amount, list_price, plan_title, status (… canceled, refunded), gateway_authority`
|
||||
- `Order user_id, plan_id, subscription_id, amount, list_price, plan_title, duration_days, status (… canceled, refunded), provider, gateway_authority, bazaar_purchase_token, bazaar_valid_until`
|
||||
- `Transaction → Order · gateway_ref_id, status, paid_at`
|
||||
- `BazaarCredential توکنِ اتصال به بازار`
|
||||
|
||||
@@ -419,7 +417,7 @@ with transaction.atomic():
|
||||
|---|---|
|
||||
| یکتا: `UserAnswer(user_id, question, idempotency_key)` | ثبتِ دوبارهی یک جواب با تکرارِ درخواست |
|
||||
| یکتا: `Redemption(کاربر، کد)` | استفادهی دوباره از یک کدِ تخفیف |
|
||||
| یکتا: `QuestionCredit(order_id)` برای ردیفِ خرید | اعتبارِ دوباره وقتی تأییدِ پرداخت دو بار برسد |
|
||||
| یکتا: `Transaction(gateway, gateway_ref_id)` | روزِ دوباره وقتی تأییدِ پرداخت دو بار برسد، یا یک توکنِ خرید برای دو سفارش |
|
||||
| یکتا: `CampaignDelivery(کمپین، کاربر، کانال)` | پیامِ تکراری وقتی کمپین دو بار همزمان اجرا شود |
|
||||
| یکتا: `LessonChunk(collection, qdrant_point_id)` و `IngestedPage(collection, page)` | تکهی تکراری در Qdrant وقتی خواندنِ کتاب از سر گرفته میشود |
|
||||
|
||||
@@ -476,16 +474,16 @@ with transaction.atomic():
|
||||
| POST | `/api/characters/select` | ورود | انتخابِ همراه (فقط بازشده) |
|
||||
| **دستیار — `ai_tutor` و `rag`** | | | |
|
||||
| POST | `/api/ai/ask` | ورود | پرسش؛ JSON یا استریم |
|
||||
| GET | `/api/ai/quota` | ورود | سوالهای رایگانِ باقیِ امروز و اعتبارِ بسته |
|
||||
| GET | `/api/ai/quota` | ورود | سوالهای باقیِ امروز |
|
||||
| GET | `/api/ai/conversations/{id}/messages` | ورود | تاریخچهی گفتگو |
|
||||
| POST | `/api/ai/messages/{id}/feedback` | ورود | 👍/👎 روی یک جواب |
|
||||
| POST | `/api/rag/ingest/text` · `/pdf` | داخلی | افزودنِ متن یا PDF به Qdrant |
|
||||
| GET | `/api/rag/chunks` | داخلی | مرورِ تکههای ذخیرهشده |
|
||||
| **بسته، پرداخت، تخفیف** | | | |
|
||||
| GET | `/api/subscription/plans` | عمومی | بستههای سوالِ در حالِ فروش |
|
||||
| GET | `/api/subscription/plans` | عمومی | اشتراکهای در حالِ فروش |
|
||||
| POST | `/api/subscription/purchase` | ورود | شروعِ خرید؛ سفارش و لینکِ پرداخت |
|
||||
| GET | `/api/subscription/status` | ورود | اعتبارِ سوال، قلبِ نامحدود تا کِی، اشتراکِ قدیمی (و بازسنجیِ پرداختِ جاافتاده) |
|
||||
| POST | `/api/payment/orders` | داخلی | ساختِ سفارش (سفارشِ بازار، با `planId` ِ بسته) |
|
||||
| POST | `/api/payment/bazaar/orders/{id}/start` | ورود | شروعِ خریدِ بازار (SKU و قیمتِ امضاشده) |
|
||||
| GET | `/api/payment/orders/{id}` | ورود | وضعیتِ سفارش |
|
||||
| GET | `/api/payment/invoices` | ورود | پرداختها و فاکتورهای کاربر (شمارهی فاکتور، تخفیف، کدِ پیگیری) |
|
||||
| GET | `/api/payment/orders/{id}/pay` | عمومی | رفتن به درگاهِ زرینپال |
|
||||
@@ -700,7 +698,7 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
|
||||
| انتخابِ مدلِ جواب: ۱۰۲ مسئلهی تصحیحشدهی دستی | `gpt-4.1-mini` : ۹۵–۹۶ درست · `gpt-4o-mini` : ۸۵–۹۰ |
|
||||
| شکلها: ۶۴ مسئلهی تصادفیِ کتاب، وارسیِ چشمیِ تکتک | ۴۰ درست، ۱۲ قابلِ قبول، ۷ غلط، ۵ بیشکل — حدودِ یکی از پنج هنوز ایراد دارد؛ برای همین شکل همیشه کنارِ حلِ متنی میآید |
|
||||
| سرعت | جوابِ کامل با شکل ~۳۰ ثانیه؛ اندازهگیری شد که در خودِ GapGPT است («فقط بگو سلام» = ۷ ثانیه) |
|
||||
| سقفها | ۲ سؤالِ رایگان در روز (مشترک ۳)، بعد از اعتبارِ بسته؛ ۲۰ در ساعت؛ مهمانها بر اساسِ IP (تا با ساختِ حسابِ مهمانِ تازه دور زده نشود) |
|
||||
| سقفها | ۲ سؤالِ رایگان در روز (مشترک ۴)؛ ۲۰ در ساعت؛ مهمانها بر اساسِ IP (تا با ساختِ حسابِ مهمانِ تازه دور زده نشود) |
|
||||
|
||||
### ۹-۷ فقط ریاضی، و چه چیزی هزینه دارد
|
||||
|
||||
@@ -708,7 +706,7 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
|
||||
|
||||
| قاعدهی خرج | |
|
||||
|---|---|
|
||||
| ترتیب | اول سقفِ روزانه (۲ رایگان، مشترک ۳)، بعد اعتبارِ بسته ( `Message.charge` : `free` / `credit` / `none` ) |
|
||||
| ترتیب | فقط سقفِ روزانه (۲ رایگان، مشترک ۴؛ `Message.charge` : `free` / `none` ) |
|
||||
| خرج میشود | هر سوالی که به مدل برسد، از جمله ردِ «غیرِ ریاضی» توسطِ مدل |
|
||||
| خرج نمیشود | ردِ صافیِ سرور، جوابِ کششده، جوابی که ساخته نشد، و تکرارِ درخواست با همان `Idempotency-Key` |
|
||||
| همزمانی | دو سوالِ همزمان با یک اعتبار فقط یک جواب میگیرند؛ با دو thread واقعی آزموده میشود ( `ai_tutor/tests_credit_concurrency.py` ) |
|
||||
@@ -788,7 +786,7 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
|
||||
|
||||

|
||||
|
||||
*شکلِ ۱۱-۳ — صفحهی «قلبهات تموم شد»: شمارشِ معکوسِ زنده تا قلبِ بعدی، قلبی که کمکم پر میشود، ساعتِ پر شدنِ همه، پیشنهادِ بستهها و «تا اون موقع از پی بپرس» (پرسیدن قلب خرج نمیکند). وقتی وقتش رسید، صفحه خودش از سرور میپرسد و «ادامه بده» را نشان میدهد. فرانت: `src/components/NoHeartsScreen.tsx` .*
|
||||
*شکلِ ۱۱-۳ — صفحهی «قلبهات تموم شد»: شمارشِ معکوسِ زنده تا قلبِ بعدی، قلبی که کمکم پر میشود، ساعتِ پر شدنِ همه، پیشنهادِ اشتراک و «تا اون موقع از پی بپرس» (پرسیدن قلب خرج نمیکند). وقتی وقتش رسید، صفحه خودش از سرور میپرسد و «ادامه بده» را نشان میدهد. فرانت: `src/components/NoHeartsScreen.tsx` .*
|
||||
|
||||
## ۱۲. امنیت و احرازِ هویت
|
||||
|
||||
@@ -810,7 +808,7 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
|
||||
| دسترسی به دادهی دیگران | شناسهی کاربر همیشه از توکن خوانده میشود، هرگز از ورودی؛ هر کاربر فقط دادهی خودش را میبیند (گزارشِ والدین هم در حسابِ خودِ دانشآموز است). | همهی `views.py` ها |
|
||||
| سقفِ نرخ | روی همهی نقاطِ حساس (ورود، دستیار، خرید، کدِ تخفیف، جواب…)؛ مهمان بر اساسِ IPِ واقعی ( `NUM_PROXIES=2` : آروان + nginx). | `config/settings.py` |
|
||||
| پیامک و هزینهاش | شماره به `09XXXXXXXXX` نرمال میشود؛ فاصله بینِ دو درخواست، ۱۰ در روز برای هر شماره، ۳۰۰ در روز برای کلِ سامانه (ضدِ SMS pumping) — شمارش در دیتابیس، نه حافظهی موقت. | `accounts/phone.py` · `tests_otp_limits.py` |
|
||||
| پرداخت | فقط تأییدِ سرور-به-سرور؛ برگشتِ جعلی اثری ندارد؛ webhookِ عمومیِ ناامن حذف شد؛ هر درگاهی بیکلید «بسته» میماند (fail-closed)، نه موفقیتِ ساختگی. اعتبارِ بسته دفتری است و هر سفارش فقط یک بار اعتبار میدهد؛ وضعیتِ سفارش در ادمین فقط با actionِ لغو/بازپرداخت عوض میشود. | `payment/` · `subscription/` |
|
||||
| پرداخت | فقط تأییدِ سرور-به-سرور؛ برگشتِ جعلی اثری ندارد؛ webhookِ عمومیِ ناامن حذف شد؛ هر درگاهی بیکلید «بسته» میماند (fail-closed)، نه موفقیتِ ساختگی. هر سفارش فقط یک بار روز میدهد (قیدِ یکتا روی تراکنش)؛ وضعیتِ سفارش در ادمین فقط با actionِ لغو/بازپرداخت عوض میشود. | `payment/` · `subscription/` |
|
||||
| تقلبِ امتیاز | امتیاز فقط برای جوابِ ثبتشده؛ جایزه فقط بارِ اول. | `content/views.py` |
|
||||
| درخواستِ تکراری و همزمانی | Idempotency-Key در شش نقطه؛ `transaction.atomic` ؛ قیدِ یکتا در دیتابیس؛ SQLite در حالتِ `IMMEDIATE` تا قفلها واقعاً قفل کنند. | `config/settings.py` |
|
||||
| خطا و لاگ | هر خطا JSONِ یکدست بیجزئیاتِ داخلی؛ لاگ بدونِ بدنه، هدر، توکن یا رمز. | `config/exceptions.py` · `middleware.py` |
|
||||
@@ -967,8 +965,8 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
||||
| پیامکِ ورود | کار میکند | کدِ واقعی به گوشی رسید (الگوی SMSVerify)؛ هر پیامک هزینه دارد |
|
||||
| دستیار + شکل | کار میکند | کُند (~۳۰ ثانیه، از سمتِ GapGPT)؛ ~یک شکل از پنج ایراد دارد |
|
||||
| زرینپال | نیمه | درخواستِ پرداخت پذیرفته میشود؛ پرداختِ واقعی تا انتها آزموده نشده |
|
||||
| کافهبازار | منتظر | اتصالِ API برقرار؛ منتظرِ تعریفِ بستهها در پنلِ بازار (محصولِ مصرفی) و اپِ اندرویدی |
|
||||
| بستهی سوال و قلب | زنده | سه بسته روی سایت؛ قلبِ صفر بازی را متوقف میکند |
|
||||
| کافهبازار | منتظرِ اپ | اتصالِ API برقرار؛ سه اشتراک در پیشخان (۱۰۳۴۲–۱۰۳۴۴) و روی پلنها؛ منتظرِ ساختِ اپِ اندروید و یک خریدِ واقعی |
|
||||
| اشتراک و قلب | زنده | سه اشتراک روی سایت؛ قلبِ صفر بازی را متوقف میکند |
|
||||
| پیامِ گروهی | پایه | ذخیره و هدفگیری هست؛ زمانبندیِ خودکار نه |
|
||||
| بکاپِ خودکار | ندارد | دستی پیش از هر انتشار |
|
||||
|
||||
@@ -978,8 +976,8 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
||||
|---|---|
|
||||
| عوض کردنِ کلیدِ امضا | کد آماده؛ همه یک بار از حساب بیرون میافتند — زمانش را باید تعیین کرد. |
|
||||
| پاکسازیِ دادهی دمو | حسابهای آزمایشیِ شمارهدار ۱۰ مهر پاک شدند (با بکاپ)؛ ۷ شمارهی نمایشی و حسابهایشان عمداً ماندهاند. |
|
||||
| قیمتِ بستهها و سهمیهی رایگان | بعد از دو هفته مصرفِ واقعی بازبینی شود؛ سود در `docs/ai-cost-and-guardrails.md` حساب شده. |
|
||||
| سیاستِ بازپرداخت | الان فقط اعتبارِ خرجنشده پس گرفته میشود؛ پول از پنلِ درگاه. |
|
||||
| سقفِ دستیار و سهمیهی رایگان | بعد از دو هفته مصرفِ واقعی بازبینی شود؛ سود در `docs/ai-cost-and-guardrails.md` حساب شده. |
|
||||
| سیاستِ بازپرداخت | الان روزهای اشتراکِ همان سفارش پس گرفته میشود؛ پول از پنلِ درگاه. |
|
||||
| Redis و Postgres | هماهنگی با ادمینِ سرور؛ هر دو روی همان ماشین موجودند. |
|
||||
| پورتِ ۸۰ و دسترسیِ SSH | کارِ ادمینِ سرور. |
|
||||
| پیشرفت با هر نمره | در حالِ حاضر جزیره با هر نمرهای تمام میشود؛ اگر حدِ قبولی لازم است، یک تصمیمِ آموزشی است. |
|
||||
@@ -1050,7 +1048,7 @@ npm run build # type-check + build در dist/
|
||||
| `docs/security-before-production.md` | ۱۸ موردِ امنیتیِ باز با خطر و راهِ بستن |
|
||||
| `docs/content-authoring.md` | ویرایشِ محتوا و ساختِ انبوهِ سؤال |
|
||||
| `docs/extraction-playbook.md` | خواندنِ کتاب با مدلِ بینایی: هزینه و درسها |
|
||||
| `docs/ai-cost-and-guardrails.md` | هزینهی هر سوالِ دستیار، بستهها و قاعدهی خرج، محافظِ «فقط ریاضی» |
|
||||
| `docs/ai-cost-and-guardrails.md` | هزینهی هر سوالِ دستیار، سودِ هر اشتراک و قاعدهی خرج، محافظِ «فقط ریاضی» |
|
||||
| `docs/model-prices.md` | دفترِ قیمتِ اندازهگیریشدهی مدلها، با تاریخ؛ اگر قیمت عوض شد ردیف اضافه شود |
|
||||
| `data/textbooks/README.md` | افزودنِ کتابِ تازه |
|
||||
| `/api/docs/` | قراردادِ زنده و قابلِ امتحانِ API |
|
||||
|
||||
Reference in New Issue
Block a user