Harden auth, AI tutor, payments, and subscriptions

This commit is contained in:
2026-08-22 04:42:36 +03:30
parent f945f960c9
commit cb18c93a3e
41 changed files with 2202 additions and 595 deletions

View File

@@ -5,21 +5,13 @@ class FamilyLink(models.Model):
"""
اتصال حساب والد به یک فرزند. طبق سند معماری، این اپ فقط به accounts.User
ارجاع سبک (parent_user_id / child_user_id) می‌ده، نه FK واقعی.
⚠️ افزوده نسبت به سند اولیه: سند models فقط parent_user_id/child_user_id
را مشخص کرده بود، ولی برای پیاده‌سازی واقعی جریان «دعوت با کد» (طبق سند
Endpointها: POST /api/family/link/invite و /api/family/link/accept) باید
خود کد دعوت و شماره‌ی فرزندی که هنوز ثبت‌نام نکرده جایی نگه داشته بشه؛
دقیقاً همون الگویی که accounts.OTPCode برای نگه‌داشتن کد یک‌بارمصرف
استفاده می‌کنه. اگه این دو فیلد قرار بود جای دیگه‌ای (مثلاً یک مدل
Invite جدا) نگه داشته بشن، می‌شه بعداً جابه‌جا کرد.
"""
STATUS_CHOICES = [('pending', 'در انتظار'), ('accepted', 'پذیرفته‌شده')]
parent_user_id = models.IntegerField(db_index=True) # ارجاع سبک به accounts.User
child_user_id = models.IntegerField(null=True, blank=True, db_index=True) # ارجاع سبک به accounts.User
child_phone = models.CharField(max_length=15, blank=True) # <-- افزوده شده
invite_code = models.CharField(max_length=10, unique=True) # <-- افزوده شده
child_phone = models.CharField(max_length=15, blank=True)
invite_code = models.CharField(max_length=10, unique=True)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='pending')
invited_at = models.DateTimeField(auto_now_add=True)
accepted_at = models.DateTimeField(null=True, blank=True)
@@ -27,15 +19,7 @@ class FamilyLink(models.Model):
class Meta:
ordering = ['-invited_at']
constraints = [
# 🔒 اصلاح race condition: قبلاً فقط با یک چکِ application-level
# (filter().first() قبل از create) جلوی دعوتِ تکراری برای یک
# parent/child_phone گرفته می‌شد که در برابر دو درخواستِ هم‌زمان
# کافی نبود (هر دو می‌تونستن ببینن که pending وجود نداره و هر دو
# create کنن). این UniqueConstraint سطح DB تضمین می‌کنه که برای
# هر (parent_user_id, child_phone) فقط یک دعوتِ pending هم‌زمان
# می‌تونه وجود داشته باشه؛ FamilyLinkInviteView حالا
# IntegrityError ناشی از این قید رو می‌گیره و رکورد pending
# موجود رو برمی‌گردونه (idempotent).
# حداکثر یک دعوت pending هم‌زمان برای هر (parent_user_id, child_phone).
models.UniqueConstraint(
fields=['parent_user_id', 'child_phone'],
condition=models.Q(status='pending'),