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

@@ -225,7 +225,6 @@ INSTALLED_APPS = [
'notifications',
'affiliate',
'support',
'family',
# لاگِ عمومیِ تغییرِ وضعیت، مستقل از بقیه‌ی اپ‌ها
'statuslog',
@@ -475,8 +474,6 @@ REST_FRAMEWORK = {
'offer_redeem': '10/hour',
'discount_validate': '20/hour',
'discount_redeem': '10/hour',
'family_invite_accept': '10/hour',
'family_invite_create': '5/hour',
'user_profile_write': '60/hour',
'affiliate_track': '10/hour',
'payment_webhook': '60/hour',
@@ -583,7 +580,6 @@ SPECTACULAR_SETTINGS = {
{'name': 'Payment (payment)', 'description': 'سفارش و تراکنش (زرین‌پال و کافه‌بازار)'},
{'name': 'Discount (discount)', 'description': 'کدِ تخفیف و آفرها'},
{'name': 'Notifications (notifications)', 'description': 'اعلان‌های کاربر'},
{'name': 'Family (family)', 'description': 'کنترلِ والدین و گزارشِ هفتگی'},
{'name': 'Support (support)', 'description': 'تیکت و گزارشِ اشکال'},
{'name': 'Affiliate (affiliate)', 'description': 'برنامه‌ی معرفی و پورسانت'},
{'name': 'System', 'description': 'سلامتِ سرویس'},
@@ -813,6 +809,9 @@ OPENAI_BASE_URL = os.environ.get('OPENAI_BASE_URL', 'https://api.gapgpt.app/v1')
# ۸۵–۹۰ِ gpt-4o-mini، با ۲٫۹ برابر هزینه (۰٫۱۲ سنت برای هر پاسخ). شرح: گزارش ۲۳ و
# docs/architecture.md. پیش‌فرض همین‌جاست تا دیپلوی به ویرایشِ دستیِ .env نیاز نداشته باشد.
OPENAI_CHAT_MODEL = os.environ.get('OPENAI_CHAT_MODEL', 'gpt-4.1-mini')
# سقفِ سوالِ روزانه از دستیار برای کاربرِ بدونِ اشتراک (مشترکان: Plan.ai_daily_quota).
# ۳ پیشنهادِ docs/ai-cost-and-guardrails.md است؛ تا ۲۰۲۶-۱۰-۰۱ برای همه ۱۵ بود.
AI_DAILY_QUOTA_FREE = int(os.environ.get('AI_DAILY_QUOTA_FREE', '3'))
# اختیاری: مدلِ جدا فقط برای پایه‌های دهم تا دوازدهم (حسابان/مثلثات). خالی یعنی
# همه‌ی پایه‌ها OPENAI_CHAT_MODEL. انتخابش با `rag_model_benchmark` سنجیده می‌شود.
OPENAI_CHAT_MODEL_UPPER = os.environ.get('OPENAI_CHAT_MODEL_UPPER', '')

View File

@@ -16,7 +16,6 @@ from accounts.authentication import issue_access_token
from accounts.models import User
from content.models import Choice, Grade, Lesson, Node, Question, Unit
from discount.models import Offer
from family.models import FamilyLink
from payment.models import Order
from payment.services import apply_order_payment_result
from subscription.models import Plan, Subscription
@@ -100,7 +99,9 @@ class ZarinpalErrorBodyReturnsToApp(_Base):
class WeeklyReportCountsIslands(TestCase):
def test_island_quiz_xp_reaches_the_parent(self):
# تا ۲۰۲۶-۱۰-۰۱ همین سناریو گزارشِ والد را از /api/family/weekly-report می‌خواند؛
# آن اپ حذف شد و گزارش حالا در حسابِ خودِ دانش‌آموز است.
def test_island_quiz_xp_reaches_the_weekly_report(self):
cache.clear()
grade = Grade.objects.create(title='پایه نهم', icon='', color='')
node = Node.objects.create(unit=Unit.objects.create(grade=grade, title='فصل', order=1),
@@ -118,14 +119,11 @@ class WeeklyReportCountsIslands(TestCase):
child_client.post(f'/api/nodes/{node.id}/quiz/complete',
{'correctCount': 4, 'totalQuestions': 4}, format='json')
parent = User.objects.create(phone='09120008003')
FamilyLink.objects.create(parent_user_id=parent.id, child_user_id=child.id,
child_phone=child.phone, invite_code='654321',
status='accepted', accepted_at=timezone.now())
report = _client(parent).get('/api/family/weekly-report').json()
self.assertEqual(report['xpEarned'], 40)
self.assertEqual(report['islandsCompleted'], 1)
self.assertEqual(report['lessonsCompleted'], 0)
week = child_client.get('/api/user/weekly-report').json()['thisWeek']
self.assertEqual(week['xp'], 40)
self.assertEqual(week['islands'], 1)
self.assertEqual(week['lessons'], 0)
self.assertEqual((week['questions'], week['correct']), (4, 4))
class AskWithNonNumericQuestionId(TestCase):

View File

@@ -89,5 +89,4 @@ urlpatterns = [
path('api/', include('notifications.urls')),
path('api/', include('affiliate.urls')),
path('api/', include('support.urls')),
path('api/', include('family.urls')),
]