Audit fixes 2026-10-03: seed keeps admin edits, plan cache, refresh limit, payment and tutor guards

- seed_content no longer undoes admin work: a SeedRecord ledger (content/seed_sync.py)
  stops deleted/renamed/re-worded content from being recreated and keeps admin-edited
  lesson text; new rows in the seed files still arrive (migration content.0010)
- plan list cache is invalidated on Plan save (receiver used a stale 'v1' key)
- token refresh per-IP cap 30/hour -> 600/hour (TOKEN_REFRESH_IP_RATE)
- a failed gateway result only changes a pending order (forged NOK no longer
  overwrites refunded/canceled)
- tutor gets the correct answer only after the student has answered
- support reply notification moved to a TicketMessage post_save signal
- global daily cap on paid AI questions (AI_GLOBAL_DAILY_MAX, default 3000)
- Bazaar verify uses only the order's SKU; bazaar orders require a SKU
- /api/health warns on the committed signing key; deploy.sh saves local commits;
  400 for bad ?unit= / ?period=; account delete keeps the referrer's Conversion
- earlier today: no stub answers outside DEBUG, ?grade= removed, logged fallbacks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 16:05:27 +03:30
parent 7e027c167c
commit ab5e53730a
31 changed files with 623 additions and 49 deletions

View File

@@ -318,3 +318,25 @@ class QuestionReport(models.Model):
def __str__(self):
return f'report-{self.pk} on q{self.question_id} ({self.get_reason_display()})'
class SeedRecord(models.Model):
"""
چیزی که `seed_content` یک بار از فایل‌های ریپو تحویل داده است (content/seed_sync.py).
چرا: پیش از ۲۰۲۶-۱۰-۰۳ هر دیپلوی هر چه در فایل بود و در دیتابیس پیدا نمی‌شد را دوباره
می‌ساخت. سوالی که ادمین پاک کرده بود برمی‌گشت، سوالی که صورتش را اصلاح کرده بود با صورتِ
قدیم کنارش تکرار می‌شد، فصلی که نامش عوض شده بود با همه‌ی درس‌ها و سوال‌هایش دوباره ساخته
می‌شد، و متنِ توضیحِ درس که در ادمین اصلاح شده بود با متنِ فایل بازنویسی می‌شد. این دفتر
می‌گوید «این را قبلاً داده‌ایم»؛ اگر حالا نیست، کارِ ادمین بوده و دوباره ساخته نمی‌شود.
`value` فقط برای متنِ توضیحِ درس است: هشِ آخرین متنی که سید نوشت. اگر متنِ دیتابیس هنوز
همان است، ادمین دستش نزده و متنِ تازه‌ی فایل جایش می‌نشیند؛ وگرنه متنِ ادمین می‌ماند.
"""
key = models.CharField(max_length=40, unique=True) # sha1 از نوع و مسیرِ عنوان
kind = models.CharField(max_length=10)
value = models.CharField(max_length=40, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'seed-{self.kind}-{self.key[:8]}'