- payment/providers.py: ZarinpalProvider / BazaarProvider with one interface (check_plan, sku_for, payment_url,
purchase_extras, discount_blocker, reconcile_user); provider_for / provider_for_method pick one; unknown fails.
create_order, purchase view, discount redeem and reconcile_user_orders use it instead of scattered ifs.
- subscription.0015: Bazaar SKU 10342 on the one-month plan (owner: "probably the monthly one").
- config/health: the Bazaar check no longer swallows its exception without a log.
982 tests green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
- content: unit_is_free/has_unit_access; questions, node quiz, answer and lesson/node
complete return 402 subscription_required outside the first unit without an active
subscription. /api/home units carry requiresSubscription.
- subscription.0013: monthly 498k, quarterly 798k, six-month 1,298k toman, unlimited
hearts, 3 AI questions/day; packs stay on sale for more questions.
- AI_DAILY_QUOTA_FREE default 3 -> 2.
- /api/subscription/status adds planTitle, planDurationDays, aiDailyQuota.
- tests: content/tests_paywall.py; existing unlock-order tests get a subscription.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- content: POST nodes/{id}/quiz/complete and lessons/{id}/complete refuse a locked island
(403 node_locked, same rule as /api/home) and a user who has not answered every question
(409 answers_missing). Before, only a user with zero hearts and no answers was refused, so
anyone else finished any island, locked or not, and finishing a locked island's lessons
unlocked it. A node or lesson with no questions still passes when unlocked, with no XP.
The client can no longer understate totalQuestions to buy stars.
- throttle_scope on 15 routes that had none (question bank without login, intro, hearts,
notifications, tickets, plans, subscription status, orders, offers, affiliate, assistant
history and feedback, logout).
- admin: OTPCode and Session can only be added/changed by a superuser.
- deploy.sh: integrity-checked DB backup before migrate (keeps the last 2); no migrate without it.
- docs and CLAUDE.md updated to say what is verified and what is not.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
accounts.0008 drops the columns and refuses to run while guest rows exist.
Docs describe the current state; tests that only proved guest behaviour are gone.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- packs (20 / 60 / 150 questions for 45,000 / 119,000 / 259,000 toman) never expire and
give 7 / 30 / 60 days of unlimited hearts; old subscription plans stay for existing
subscribers but are no longer on sale
- credit ledger (subscription.QuestionCredit): +N per paid pack (once per order), -1 per
tutor question that reaches the API; the free daily questions are spent first; questions
stopped by the server filter, cached answers and failed answers cost nothing
- unlimited hearts from packs live in HeartsPass, so buying a pack never rewrites a
running subscription
- Message.charge records how each question was paid; GET /api/ai/quota and the ask
responses report free-left-today and credits
- HEARTS_ENFORCED now defaults to on: zero hearts stops answering until refill or a pack
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- profile publicId ("User482917", accounts/public_id.py); admin shows and searches it
- plans API reports aiDailyQuota per plan and freeAiDailyQuota; migration 0007 sets the
temporary prices (monthly 99,000 / yearly 990,000 toman, 5 tutor questions a day) unless
an admin already edited the yearly plan
- tutor tone follows the learner's age (18+ gets an adult tone at any grade); notation
still follows the grade's book; answer cache keyed by tone (prompt tutor-v12-age-tone)
- POST /api/user/delete: phone users confirm with a fresh SMS code (same checks as login,
now shared in verify_phone_otp), guests type "حذف"; payment records are kept, demo
accounts refused
- delete_users --skip-paid
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- remove the family app from the root (separate parent accounts, invite codes, parental
settings); progress.0006 drops its tables, permissions and migration history, and
Profile.parent_phone, the two family notification types and ?userId= on
/api/progress/nodes go with it
- GET /api/user/weekly-report: last 7 days next to the 7 before (estimated minutes,
questions, accuracy, islands, tutor questions) for the "parent report" page
- profile birthDate in Jalali (accounts/jalali.py), ages 5-99, birthYear derived from it
- GET /api/payment/invoices; orders now snapshot list price and plan title
- tutor answers only math: server pre-filter, scope rule in the prompt (tutor-v11),
refusal marker replaced with fixed text; self-harm gets a help line instead
- streamed answers now record token usage (it was always 0)
- daily tutor quota: AI_DAILY_QUOTA_FREE (default 3) vs Plan.ai_daily_quota
- delete_users management command for wiping test accounts across every app
- docs/ai-cost-and-guardrails.md: measured cost per question and guardrail results
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kavenegar is being switched on, so every OTP request now costs credit:
- phone numbers are normalised to 09XXXXXXXXX everywhere (login, OTP,
family invite, TEST_LOGIN_PHONES keys) and existing rows are migrated;
non-Iranian and landline numbers are rejected before any SMS
- cooldown and a daily per-phone cap are counted in the database, so the
three gunicorn workers no longer triple them
- a global daily OTP SMS cap (default 300) stops SMS pumping
- per-IP rates widened for CGNAT / school Wi-Fi (env-overridable)
- provider message id is logged; SMS text warns not to share the code
Second audit round: an offer no longer shortens a longer paid plan,
Zarinpal error bodies no longer 500, island XP reaches the parent report,
a non-numeric questionId no longer 500s after the answer is generated.
596 tests green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tutor (prompt tutor-v3-stepwise-plain): plain text instead of LaTeX/markdown,
with rag/services/mathtext.py converting whatever still slips through; full
step-by-step solutions with a check before the final answer; three tones by
grade; the last 6 messages of the conversation go to the model and follow-ups
are no longer served from another user's cache; book solutions the checker
disputes are kept out of the context; a questionId gives the tutor the
practice question and the student's wrong answer. /api/health reports when
the AI credit runs out. rag_model_benchmark compares chat models on verified
book problems (run on the server; credit ran out mid-run).
Map: only lessons that have questions count toward finishing an island,
lessonIds[0] / nextLessonId is always the next lesson to play, and a finished
island stays finished. The Seyar characters (sigma/delta/pi) are seeded.
Audit fixes, each reproduced before fixing:
- the plan is stored on the order, so paying a monthly order after starting
a yearly checkout no longer grants a year
- opening checkout no longer demotes an active subscription; renewals extend
from the current expiry; expired subscriptions are not reported active
- one discount code per order and only before the gateway; a 100% code
completes the purchase; lost Zarinpal confirmations are re-verified
- the unused /api/payment/webhook "mark paid" endpoint is removed
- XP counts only answers recorded on the server
- 6-digit OTP, each code dies after 5 wrong guesses; POST /api/auth/logout
- a guest who signs up keeps their account and progress (test phones excluded)
- affiliate commission on the first real payment; guests excluded
- parents with several children no longer get a 500
- SQLite runs transactions IMMEDIATE, since it ignores select_for_update
- admin login is rate-limited; display names drop phone numbers and links;
userQuestion is capped at 2000 characters
- server-side heart enforcement exists behind HEARTS_ENFORCED (off until the
frontend handles 403 no_hearts)
Not done on purpose: rotating the server's secret key and moving to Postgres.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
کارتِ اشتراک زیرِ هر پلن فهرستِ امکانات و یک برچسب («محبوبترین»، «بهترین
ارزش») نشان میدهد، و هر دو در کدِ فرانت هاردکد بودند — یعنی عوض کردنِ یک
بندِ متنی به دیپلویِ تازهی اپ نیاز داشت.
Plan حالا features و badge دارد. `popular` سرِ جایش ماند: بولین است و متنِ
برچسب را نمیدهد، پس این جایگزینش نیست، کنارش است. هیچکدام اجباری نیستند و
پلنهای موجود بدونشان همچنان سرو میشوند.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
subscription/views.py
- PlansListView: served from cache (subscription:plans:v1, 1h TTL);
plans change ~never and the list is read on every subscription screen.
subscription/receivers.py (new)
- post_save/post_delete on Plan -> cache.delete(PLANS_CACHE_KEY).
subscription/apps.py
- SubscriptionConfig.ready() imports receivers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>