senior's post-demo changes: parent report in the student's own account, Jalali birth date, invoices, math-only tutor, priced quotas

- 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>
This commit is contained in:
2026-10-02 02:38:43 +03:30
parent d5dbd1b23d
commit 39247f6d53
67 changed files with 1619 additions and 1140 deletions

View File

@@ -4,9 +4,9 @@
یک کلید طبیعی/یکتا (phone، device_id، code، idempotency_key و ...) انجام
می‌شه، نه با create ساده.
هدف: پوشش کمینه‌ی جریان‌های اصلی API در هر ۱۱ اپ (accounts، content،
هدف: پوشش کمینه‌ی جریان‌های اصلی API در هر ۱۰ اپ (accounts، content،
progress، ai_tutor، subscription، payment، discount، notifications،
affiliate، support، family) با کمترین حجم داده‌ی لازم برای این‌که هر
affiliate، support) با کمترین حجم داده‌ی لازم برای این‌که هر
endpoint اصلی بدون نیاز به ساختن دستی پیش‌نیازهاش قابل تست باشه.
⚠️ این دستور فقط برای محیط لوکال/staging است، نه پروداکشن:
@@ -44,8 +44,6 @@ from accounts.models import OTPCode, Profile, Session, User
# چون این فقط دیتای لوکال/staging است نه پروداکشن، همین کافیه. با پیشوند
# یکسان و بازه‌ی اختصاصی، شناساییِ رکوردهای seed در دیتابیس هم ساده می‌مونه.
MAIN_PHONE = '09120000001'
PARENT_PHONE = '09120000002'
CHILD_PHONE = '09120000003'
REFERRED_PHONE = '09120000004'
GUEST_DEVICE_ID = 'seed-guest-device-0001'
@@ -101,14 +99,13 @@ class Command(BaseCommand):
from affiliate.models import AffiliateAccount, Conversion
from content.models import Grade, NodeProgress, UserAnswer, UserLessonCompletion
from discount.models import DiscountCode, Offer, Redemption, UserOffer
from family.models import FamilyLink
from notifications.models import Notification
from payment.models import Order
from progress.models import LeaderboardEntry, StreakRecord, UserHearts, UserStats
from subscription.models import Plan, Subscription
from support.models import BugReport, Ticket
phones = [MAIN_PHONE, PARENT_PHONE, CHILD_PHONE, REFERRED_PHONE]
phones = [MAIN_PHONE, REFERRED_PHONE]
user_ids = list(User.objects.filter(phone__in=phones).values_list('pk', flat=True))
guest = User.objects.filter(device_id=GUEST_DEVICE_ID, is_guest=True).first()
if guest:
@@ -132,7 +129,6 @@ class Command(BaseCommand):
Conversion.objects.filter(referred_user_id__in=user_ids).delete()
Ticket.objects.filter(user_id__in=user_ids).delete()
BugReport.objects.filter(user_id__in=user_ids).delete()
FamilyLink.objects.filter(parent_user_id__in=user_ids).delete()
from progress.models import Character
Character.objects.filter(slug__startswith='seed-').delete()
@@ -154,7 +150,7 @@ class Command(BaseCommand):
# -- seed -------------------------------------------------------------
def _seed(self):
main_user, guest_user, parent_user, child_user, referred_user = self._seed_accounts()
main_user, guest_user, referred_user = self._seed_accounts()
self._seed_content_and_progress(main_user)
self._seed_characters()
self._seed_ai_tutor(main_user)
@@ -163,12 +159,10 @@ class Command(BaseCommand):
self._seed_notifications(main_user)
self._seed_affiliate(main_user, referred_user)
self._seed_support(main_user)
self._seed_family(parent_user, child_user)
tokens = [
('کاربر اصلی (ثبت‌نام‌شده)', main_user, issue_access_token(main_user.pk)),
('کاربر مهمان', guest_user, issue_access_token(guest_user.pk)),
('والد', parent_user, issue_access_token(parent_user.pk)),
]
return {'tokens': tokens}
@@ -179,7 +173,7 @@ class Command(BaseCommand):
defaults=dict(name=f'{SEED_TAG} کاربر اصلی', is_guest=False, grade_id=None),
)
Profile.objects.get_or_create(
user=main_user, defaults=dict(birth_year=2015, parent_phone=PARENT_PHONE, bio=f'{SEED_TAG} پروفایل تستی')
user=main_user, defaults=dict(birth_year=1394, bio=f'{SEED_TAG} پروفایل تستی')
)
Session.objects.get_or_create(
user=main_user,
@@ -206,18 +200,12 @@ class Command(BaseCommand):
),
)
parent_user, _ = User.objects.get_or_create(
phone=PARENT_PHONE, defaults=dict(name=f'{SEED_TAG} والد', is_guest=False)
)
child_user, _ = User.objects.get_or_create(
phone=CHILD_PHONE, defaults=dict(name=f'{SEED_TAG} فرزند', is_guest=False)
)
referred_user, _ = User.objects.get_or_create(
phone=REFERRED_PHONE, defaults=dict(name=f'{SEED_TAG} کاربر معرفی‌شده', is_guest=False)
)
self.stdout.write(self.style.SUCCESS('accounts: 5 کاربر (اصلی/مهمان/والد/فرزند/معرفی‌شده) آماده شد.'))
return main_user, guest_user, parent_user, child_user, referred_user
self.stdout.write(self.style.SUCCESS('accounts: 3 کاربر (اصلی/مهمان/معرفی‌شده) آماده شد.'))
return main_user, guest_user, referred_user
# -- content + progress --------------------------------------------------
def _seed_content_and_progress(self, main_user):
@@ -469,28 +457,3 @@ class Command(BaseCommand):
)
self.stdout.write(self.style.SUCCESS('support: 1 تیکت با یک پیام، و 1 گزارش باگ آماده شد.'))
# -- family -----------------------------------------------------------------------
def _seed_family(self, parent_user, child_user):
from family.models import FamilyLink, ParentalSettings, WeeklyReportLog
link, _ = FamilyLink.objects.get_or_create(
parent_user_id=parent_user.pk,
child_phone=CHILD_PHONE,
defaults=dict(
child_user_id=child_user.pk,
invite_code='SEEDFAM01',
status='accepted',
accepted_at=timezone.now(),
),
)
ParentalSettings.objects.get_or_create(
family_link=link, defaults=dict(daily_limit_minutes=60)
)
WeeklyReportLog.objects.get_or_create(
family_link=link,
week_start=(timezone.now() - timedelta(days=timezone.now().weekday())).date(),
defaults=dict(sent_at=timezone.now()),
)
self.stdout.write(self.style.SUCCESS('family: 1 اتصالِ خانواده (پذیرفته‌شده) با تنظیمات و گزارشِ هفتگی آماده شد.'))