Remove silent fallbacks found in the 2026-10-03 audit

- /api/ai/ask: the stub answer is reachable only under DEBUG or
  RAG_FORCE_MOCK. Without RAG keys on the server it used to return 200 with
  "this is a stub, set OPENAI_API_KEY" and spend the student's quota; now 503.
- /api/home: the `grade` alias is gone and sending it is a 400 instead of a
  silent fallback to the user's own grade. No client sent it.
- activate_subscription logs ERROR when the paid order's plan no longer exists
  (it silently became 30 days).
- resolve_grade_title logs instead of `except: pass`; curriculum_io no longer
  reads an `intro_text` key no file contains.
- CLAUDE.md: "No silent fallbacks" rules for future agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 10:38:42 +03:30
parent 7e4b8b54a6
commit 7307bfcadb
10 changed files with 98 additions and 24 deletions

View File

@@ -253,7 +253,7 @@ def _node_status(lesson_ids, completed_ids, is_first_unlocked_candidate, quiz_co
class HomeView(APIView):
"""GET /api/home?grade — نقشه‌ی مهارت‌ها + آمار (ادغام‌شده)"""
"""GET /api/home?gradeId — نقشه‌ی مهارت‌ها + آمار (ادغام‌شده)"""
# userId همیشه از request.user.id (توکن معتبر) گرفته می‌شود، نه ورودی کلاینت (IDOR).
permission_classes = [IsAuthenticated]
@@ -265,8 +265,6 @@ class HomeView(APIView):
OpenApiParameter('gradeId', OpenApiTypes.INT, required=False,
description='پایه‌ای که نقشه‌اش خواسته می‌شود؛ '
'بدونِ آن، پایه‌ی ذخیره‌شده‌ی خودِ کاربر.'),
OpenApiParameter('grade', OpenApiTypes.INT, required=False,
deprecated=True, description='نامِ قدیمیِ gradeId.'),
],
responses=HomeResponseSerializer,
tags=['Curriculum (content)'],
@@ -278,13 +276,12 @@ class HomeView(APIView):
# خودِ کاربر نادیده می‌ماند؛ نتیجه این بود که اپ در «آشنایی» پایه را ثبت
# می‌کرد و بعد صفحه‌ی خانه خالی برمی‌گشت. حالا پارامتر اولویت دارد (برای
# مرورِ پایه‌ی دیگر) و در نبودش پایه‌ی خودِ کاربر ملاک است.
# هر دو نام پذیرفته می‌شوند. نامِ درست `gradeId` است — با بقیه‌ی API
# یکی است (profile و /user/grade هم gradeId می‌گویند) — ولی نسخه‌ی
# اولِ این اندپوینت `grade` بود و کلاینت‌هایی با همان بیرون رفته‌اند.
# پذیرفتنِ هر دو ارزان است؛ نپذیرفتن یعنی انتخابِ پایه بی‌صدا نادیده
# گرفته می‌شود و کاربر نقشه‌ی پایه‌ی دیگری را می‌بیند.
grade_param = (request.query_params.get('gradeId')
or request.query_params.get('grade'))
# فقط `gradeId` (هم‌نام با بقیه‌ی API). نامِ قدیمیِ `grade` در ۲۰۲۶-۱۰-۰۳
# حذف شد: تنها کلاینت (MathApp_frontend) آن را نمی‌فرستد. فرستادنش ۴۰۰ است،
# نه نادیده گرفتنِ بی‌صدا (که کاربر را به نقشه‌ی پایه‌ی خودش می‌برد).
if 'grade' in request.query_params:
raise ValidationError({'grade': 'این پارامتر حذف شده؛ gradeId بفرستید.'})
grade_param = request.query_params.get('gradeId')
if grade_param:
try:
grade_id = int(grade_param)