let a finished island quiz open the next island, and ground ask-AI in the student's book
The frontend plays node quizzes, but a node only completed once every one of its
8-13 lessons was completed separately, so students were stuck after the first
island of each unit. POST /api/nodes/{id}/quiz/complete now completes the node
(any score; score only sets stars/xp) and returns nextNodeId.
Also closes the other backend gaps found while auditing the frontend:
- /api/ai/ask without lessonId now picks the book from the user's grade instead
of skipping retrieval, so the "ask Pi" tab is grounded in the textbook.
- guest sessions return a refreshToken; they used to go silently 401 after 1h.
- lesson_complete throttle 10/hour -> 120/hour (a lesson here is 1-2 questions).
- progressPercent is per grade, not over all 2,617 lessons of every grade.
- a one-year plan (299,000 toman) is created by migration when no plan exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
58
subscription/migrations/0005_seed_yearly_plan.py
Normal file
58
subscription/migrations/0005_seed_yearly_plan.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
پلنِ اشتراکِ یکساله — اولین پلنِ واقعیِ سرور.
|
||||
|
||||
تا این migration روی پروداکشن هیچ Planی نبود و تبِ «اشتراک» در اپ خالی
|
||||
بود. از راهِ migration ساخته میشود نه دستی از ادمین، تا هر سرورِ تازهای
|
||||
با همان `deploy.sh` صاحبش شود.
|
||||
|
||||
قیمت تصمیمِ محصولی است و از پنلِ ادمین قابلِ تغییر است؛ این migration فقط
|
||||
وقتی میسازدش که هیچ پلنی وجود نداشته باشد، پس ویرایشِ بعدیِ ادمین را هرگز
|
||||
بازنویسی نمیکند. مبنای ۲۹۹٬۰۰۰ تومان: اپهای خودآموزِ مشابه (مثلاً زبانشناس)
|
||||
اشتراکِ سالانه را حدودِ ماهی ۴۰ هزار تومان میفروشند؛ پکیجهای ویدیوییِ
|
||||
کنکور چند میلیوناند و همردهی این اپ نیستند.
|
||||
|
||||
⚠️ تنها مزیتی که اشتراک امروز در کد واقعاً میدهد «قلبِ نامحدود» است
|
||||
(subscription.services.has_unlimited_hearts). بندهای دیگرِ features برای
|
||||
همه در دسترساند؛ اگر قرار است اشتراک چیزِ بیشتری بدهد، آن تصمیمِ محصولی است.
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
YEARLY_PLAN = {
|
||||
'title': 'اشتراک یکساله',
|
||||
'price': 299_000,
|
||||
'currency': 'IRT',
|
||||
'discount_percent': 0,
|
||||
'popular': True,
|
||||
'duration_days': 365,
|
||||
'badge': 'بهترین انتخاب',
|
||||
'features': [
|
||||
'قلبِ نامحدود؛ هیچوقت وسطِ تمرین متوقف نمیشی',
|
||||
'همهی فصلها و آزمونهای پایهات',
|
||||
'دستیارِ هوشمند بر اساسِ کتابِ درسی',
|
||||
'یک سال کامل دسترسی',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def create_yearly_plan(apps, schema_editor):
|
||||
Plan = apps.get_model('subscription', 'Plan')
|
||||
if Plan.objects.exists():
|
||||
return
|
||||
Plan.objects.create(**YEARLY_PLAN)
|
||||
|
||||
|
||||
def remove_yearly_plan(apps, schema_editor):
|
||||
Plan = apps.get_model('subscription', 'Plan')
|
||||
# فقط پلنی که خودِ این migration ساخته و هنوز خریدی ندارد.
|
||||
Plan.objects.filter(title=YEARLY_PLAN['title'], subscriptions__isnull=True).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('subscription', '0004_plan_badge_plan_features'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_yearly_plan, remove_yearly_plan),
|
||||
]
|
||||
@@ -527,6 +527,9 @@ class PlanCardContractTests(TestCase):
|
||||
def setUp(self):
|
||||
from django.core.cache import cache
|
||||
cache.clear()
|
||||
# migrationِ 0005 پلنِ یکساله را میسازد؛ این تستها شکلِ یک ردیفِ
|
||||
# مشخص را میسنجند و باید از جدولِ خالی شروع کنند.
|
||||
Plan.objects.all().delete()
|
||||
|
||||
def test_plans_carry_their_feature_list_and_badge(self):
|
||||
Plan.objects.create(title='طلایی', price=49000, popular=True,
|
||||
@@ -541,3 +544,31 @@ class PlanCardContractTests(TestCase):
|
||||
row = self.client.get('/api/subscription/plans').json()['plans'][0]
|
||||
self.assertEqual(row['features'], [])
|
||||
self.assertEqual(row['badge'], '')
|
||||
|
||||
|
||||
class YearlyPlanSeedTests(TestCase):
|
||||
"""migrationِ 0005: سرورِ بیپلن یک پلنِ یکساله میگیرد."""
|
||||
|
||||
def test_the_yearly_plan_exists_after_migrations(self):
|
||||
plan = Plan.objects.get(title='اشتراک یکساله')
|
||||
self.assertEqual(plan.duration_days, 365)
|
||||
self.assertEqual(plan.price, 299_000)
|
||||
self.assertEqual(plan.currency, 'IRT')
|
||||
self.assertTrue(plan.features)
|
||||
|
||||
def test_it_is_served_to_the_app(self):
|
||||
from django.core.cache import cache
|
||||
cache.clear()
|
||||
titles = [p['title'] for p in self.client.get('/api/subscription/plans').json()['plans']]
|
||||
self.assertIn('اشتراک یکساله', titles)
|
||||
|
||||
def test_it_never_overwrites_existing_plans(self):
|
||||
import importlib
|
||||
from django.apps import apps as global_apps
|
||||
seed = importlib.import_module('subscription.migrations.0005_seed_yearly_plan')
|
||||
Plan.objects.all().delete()
|
||||
Plan.objects.create(title='پلنِ ادمین', price=1000)
|
||||
|
||||
seed.create_yearly_plan(global_apps, None)
|
||||
|
||||
self.assertEqual(list(Plan.objects.values_list('title', flat=True)), ['پلنِ ادمین'])
|
||||
|
||||
Reference in New Issue
Block a user