tutor answers step by step in plain text, islands finish, and the audit's money and login holes are closed

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>
This commit is contained in:
2026-09-25 06:24:13 +03:30
parent fbdb9495ee
commit 42783f9ba4
56 changed files with 2792 additions and 326 deletions

View File

@@ -6,6 +6,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
Django + DRF backend for Tadrisino, an educational app (Persian/Farsi). Codebase and API responses are in Persian; comments in source are Persian too.
## Reviewing this codebase: comments are claims, not evidence
Comments here were mostly written by earlier agents to justify their own change. They
explain intent; they do not prove the code is right. An audit on 2026-09-25 found real
bugs directly under comments calling the same code "safe" or "race-free" (plan swap
at checkout, `select_for_update` guards that SQLite silently ignores).
- **When asked to find problems, review adversarially.** Judge by behaviour, not by
the comment above it: write a throwaway test or call the endpoint and watch what
happens. Ask "how would a cheating student, an attacker, or a confused parent break
this?" A finding counts once it is reproduced; say which ones were only read.
- **When writing comments, report — don't vouch.** Say what the code does and why it
was chosen, and name what was *not* checked. Avoid "this is safe / correct /
race-free / the right architecture"; write "guards X; not tested under Y" instead.
Record uncertainty plainly so the next reader re-checks rather than trusts.
## Commands
```bash
@@ -46,7 +62,7 @@ API docs: Swagger `/api/docs/`, Redoc `/api/redoc/`, admin `/admin/`.
| `campaigns` | messaging campaign storage only, no send logic yet | `MessageCampaign` |
| `config` | project settings, root urls, middleware | — |
**Auth is custom, not Django's.** `accounts.User` is deliberately *not* `AUTH_USER_MODEL` — it's an independent business model. Auth is a stateless signed bearer token (`django.core.signing`, no JWT library) issued/verified in `accounts/authentication.py` (`TadrisinoTokenAuthentication`, header `Authorization: Bearer <token>`), backed by `accounts.Session`. Missing/invalid header → anonymous request, not a hard failure; each view's permission class decides if auth is required.
**Auth is custom, not Django's.** `accounts.User` is deliberately *not* `AUTH_USER_MODEL` — it's an independent business model. Auth is a stateless signed bearer token (`django.core.signing`, no JWT library) issued/verified in `accounts/authentication.py` (`TadrisinoTokenAuthentication`, header `Authorization: Bearer <token>`). The access token is only a signed `user_id` (1 h); `accounts.Session` holds refresh tokens, and `POST /api/auth/logout` revokes older access tokens via `User.tokens_valid_after`. Missing/invalid header → anonymous request, not a hard failure; each view's permission class decides if auth is required.
**Apps are decoupled by convention**: instead of real FKs across app boundaries, code uses loose refs (`user_id`, `object_id` + `label`) so any app can be split out later. Don't add cross-app FKs — follow the existing loose-reference pattern.
@@ -56,7 +72,7 @@ API docs: Swagger `/api/docs/`, Redoc `/api/redoc/`, admin `/admin/`.
**Structured logging**: `config.middleware.APIRequestLoggingMiddleware` (last in `MIDDLEWARE`, closest to the view) logs every `/api/` request as JSON (method/path/status/duration/user, no body/headers/secrets) via `config/log_formatters.py`.
**SQLite** is current DB (`OPTIONS.timeout=20` to tolerate concurrent-write locks, e.g. campaigns tests); README notes Postgres migration is still pending.
**SQLite** is current DB (`OPTIONS.timeout=20`, and `transaction_mode: IMMEDIATE` so every `atomic()` takes the write lock up front — SQLite ignores `select_for_update`, so without it those guards locked nothing). Tests run on a file DB (`.test-db.sqlite3`). Postgres migration is still pending.
**SMS** goes through Kavenegar (`KAVENEGAR_API_KEY`/`KAVENEGAR_SENDER`); failures return explicit 503, not a fake success.
@@ -371,7 +387,45 @@ preferred setup, because there is then no key to leak. **Never let the server ru
`django-insecure-…` fallback from `config/settings.py`: that string is committed here, so
anyone who can read this repo can forge a token for any user.**
## وضعیت فعلی (۲۰۲۶-۰۹-۲۴) — این را اول بخوان
## وضعیت ۲۰۲۶-۰۹-۲۵ — این را اول بخوان
کیفیتِ دستیار و گیرِ نقشه (جزئیات و دلیل‌ها در `docs/architecture.md`). **۵۵۵ تست سبز.**
این کارها روی سرور **هنوز دیپلوی نشده‌اند**، مگر خلافش اینجا ثبت شده باشد.
- **پرامپت `tutor-v3-stepwise-plain`**: متنِ ساده (بی LaTeX/markdown، با تورِ ایمنیِ
`rag/services/mathtext.py`)، حلِ کامل و مرحله‌به‌مرحله با وارسی و «✅ جواب»، لحنِ
سه‌سطحی، حافظه‌ی ۶ پیامِ آخرِ گفتگو، و حذفِ راه‌حل‌های `disagrees` از context.
- **جزیره‌ها**: فقط درس‌های سوال‌دار شمرده می‌شوند، `nextLessonId` + `lessonIds[0]`
همیشه درسِ بعدی است، و جزیره‌ی تمام‌شده تمام می‌ماند. بدونِ این، فرانتِ فعلی
(که همیشه `lessonIds[0]` را باز می‌کند) هیچ جزیره‌ای را تمام نمی‌کرد.
- **سه‌یار** (`sigma`/`delta`/`pi`، باز شدن با ۰/۱۰۰/۳۰۰ امتیاز) با migration
`progress.0004`؛ کاتالوگ روی سرور خالی بود.
- **`/api/health` حالا تمام شدنِ اعتبارِ GapGPT را نشان می‌دهد** (`aiProvider`).
- ⛔ **اعتبارِ GapGPT در ۲۰۲۶-۰۹-۲۵ تمام شد** (وسطِ `rag_model_benchmark`؛ مانده
‎−$0.0016). تا شارژ، دستیار برای همه ۵۰۳ می‌دهد. انتخابِ مدلِ چت منتظرِ همین است:
هزینه‌ی واقعیِ هر پاسخ با پرامپتِ تازه — gpt-4o-mini ۰٫۰۴۴، gpt-4.1-mini ۰٫۱۱۶،
gpt-5-mini ۰٫۱۷۱، gpt-5.4-mini ۰٫۲۹، o4-mini ۰٫۴۱ سنت. دقتشان هنوز اندازه نشده.
**تصمیمِ صاحبِ پروژه (۰۹-۲۵): فعلاً gpt-4o-mini می‌ماند** (ارزان‌ترین). اولین
کاندیدا برای سنجش بعد از شارژ: `deepseek-v4-flash` (قیمتِ رسمی از 4o-mini کمتر،
ریاضیِ خیلی قوی‌تر، ولی فارسی و حالتِ بدونِ thinking‌اش اندازه نشده) و
`gpt-4.1-mini`. تیمِ Rag_backend برای جوابِ ریاضی `o3-mini` استفاده می‌کند.
پوشه‌ی موقتِ سنجش روی سرور: `~/mathapp-v2/.bench/` (پس از اتمام پاک شود).
- فرانت: PRِ ما (#3) بسته شد؛ هرچه فرانت باید بکند فقط در
`~/Desktop/کارهای-فرانت-تدریسینو.html` است (نه PR). شاخه‌ی محلیِ
`zareian/ai-chat-readability` فقط مرجع است.
**بازبینیِ بدبینانه‌ی همان روز و رفع‌ها** (۵۷۹ تست سبز؛ جزئیات در `docs/architecture.md`):
پلن روی سفارش (باگِ «ماهانه بده، سالانه بگیر»)، صفحه‌ی خرید اشتراکِ فعال را
دست نمی‌زند، تمدید از انقضای فعلی، یک کدِ تخفیف پیش از درگاه و کدِ ۱۰۰٪ = پرداخت،
بازسنجیِ خودکارِ پرداختِ تأییدنشده، حذفِ `/api/payment/webhook`، پورسانتِ affiliate
روی پرداختِ واقعی (مهمان بیرون)، امتیاز فقط از جوابِ ثبت‌شده، `HEARTS_ENFORCED`
(خاموش)، OTPِ ۶ رقمی با ۵ حدس، خروج از حساب (`tokens_valid_after`)، مهمانِ
ثبت‌نام‌کننده همان کاربر می‌ماند، والدِ چندفرزندی، سقفِ ورودِ ادمین، پاک‌سازیِ نامِ
نمایشی، `questionId` در دستیار، SQLite با `transaction_mode: IMMEDIATE`.
**عمداً انجام نشد** (تصمیمِ صاحبِ پروژه: برای ارائه نقطه‌ی شکست می‌سازند):
عوض کردنِ `DJANGO_SECRET_KEY`ِ سرور (همه را بیرون می‌اندازد) و مهاجرت به Postgres.
## وضعیت ۲۰۲۶-۰۹-۲۴
سرور روی `81348ce` به بعد است (سرویس واقعاً ری‌استارت شد)، **۵۳۰ تست سبز**، ۶۰ اندپوینت. **پروژه در این تاریخ بسته شد**؛ فایل‌های تحویل (گزارشِ ۲۲، راهنمای ارائه، zipِ بک‌اند) در `~/Desktop/گزارشات Tadrisino/گزارش-۲۲/` به‌روزند. آن روز کلِ
فرانت (`MathApp_frontend`، upstream `main` = `d223336`) صفحه‌به‌صفحه با API زنده