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:
2026-09-24 02:49:02 +03:30
parent 0c6b08c9ff
commit b32194497c
21 changed files with 803 additions and 12 deletions

View File

@@ -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)), ['پلنِ ادمین'])