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

27
support/receivers.py Normal file
View File

@@ -0,0 +1,27 @@
"""
اعلانِ «پاسخِ تازه برای تیکتت» با هر پیامِ پشتیبانی، از هر مسیری که ذخیره شود.
تا ۲۰۲۶-۱۰-۰۳ اعلان فقط در `TicketMessageAdmin.save_model` ساخته می‌شد. پشتیبان معمولاً از
صفحه‌ی خودِ تیکت (inline) جواب می‌دهد و آن مسیر از `TicketAdmin` رد می‌شود؛ پیام ثبت می‌شد و
کاربر هیچ اعلانی نمی‌گرفت (بازتولید شد). سیگنال روی خودِ مدل هر دو مسیر را می‌گیرد.
اعلان داخلِ همان تراکنشِ ذخیره‌ی پیام نوشته می‌شود؛ شکستش ذخیره‌ی پیام را هم برمی‌گرداند.
"""
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import TicketMessage
@receiver(post_save, sender=TicketMessage, dispatch_uid='support_notify_ticket_reply')
def _notify_ticket_reply(sender, instance, created, **kwargs):
if not created or instance.sender != 'support':
return
from notifications.services import notify
notify(
user_id=instance.ticket.user_id,
type='ticket_replied',
title='پاسخ جدید برای تیکت شما',
body=instance.text[:200],
related_id=instance.ticket_id,
)