- remove the family app from the root (separate parent accounts, invite codes, parental settings); progress.0006 drops its tables, permissions and migration history, and Profile.parent_phone, the two family notification types and ?userId= on /api/progress/nodes go with it - GET /api/user/weekly-report: last 7 days next to the 7 before (estimated minutes, questions, accuracy, islands, tutor questions) for the "parent report" page - profile birthDate in Jalali (accounts/jalali.py), ages 5-99, birthYear derived from it - GET /api/payment/invoices; orders now snapshot list price and plan title - tutor answers only math: server pre-filter, scope rule in the prompt (tutor-v11), refusal marker replaced with fixed text; self-harm gets a help line instead - streamed answers now record token usage (it was always 0) - daily tutor quota: AI_DAILY_QUOTA_FREE (default 3) vs Plan.ai_daily_quota - delete_users management command for wiping test accounts across every app - docs/ai-cost-and-guardrails.md: measured cost per question and guardrail results Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
65 lines
4.1 KiB
Python
65 lines
4.1 KiB
Python
from django.db import models
|
||
|
||
|
||
class Plan(models.Model):
|
||
"""
|
||
⚠️ اصلاح نسبت به سند اولیه: نمونهی پاسخ GET /api/subscription/plans در
|
||
سند Endpointهای API یک فیلد boolean به اسم `popular` داره («ماهانه» →
|
||
false، «سالانه» → true) که در دیاگرام مدلها (سند معماری داده) برای
|
||
Plan اصلاً ذکر نشده. چون بدون این فیلد نمیشه دقیقاً همون response
|
||
schema رو پیادهسازی کرد، اینجا اضافه شده — دقیقاً مثل فیکس is_correct
|
||
روی content.Choice در فاز ۱.
|
||
"""
|
||
title = models.CharField(max_length=50)
|
||
price = models.PositiveIntegerField()
|
||
currency = models.CharField(max_length=10, default='IRT')
|
||
discount_percent = models.PositiveSmallIntegerField(default=0)
|
||
popular = models.BooleanField(default=False)
|
||
# منبعِ صریحِ مدتِ اشتراک؛ بدونش activate_subscription مجبور بود مدت رو
|
||
# از رویِ متنِ title (مثلاً کلمهی «سالانه») حدس بزنه.
|
||
duration_days = models.PositiveIntegerField(default=30)
|
||
|
||
# کارتِ اشتراک زیرِ هر پلن فهرستِ امکانات نشان میدهد («درسهای نامحدود»،
|
||
# «دستیار هوشمند»…). تا امروز این فهرست در کدِ فرانت هاردکد بود، یعنی
|
||
# عوض کردنِ یک بند به دیپلویِ تازهی اپ نیاز داشت.
|
||
features = models.JSONField(default=list, blank=True)
|
||
# برچسبِ بالای کارت: «محبوبترین»، «بهترین ارزش». `popular` فقط بولین است
|
||
# و یک متن نمیدهد؛ این جایگزینش نیست، کنارش است.
|
||
badge = models.CharField(max_length=30, blank=True)
|
||
# چند سوال در روز از دستیار. هر سوال حدودِ ۰٫۲ سنتِ دلار (~۶۰۰ تومان) هزینهی API
|
||
# دارد (docs/ai-cost-and-guardrails.md)، پس این عدد مستقیم سودِ پلن را تعیین میکند.
|
||
# کاربرِ بدونِ اشتراک سقفِ AI_DAILY_QUOTA_FREE (settings) را دارد.
|
||
ai_daily_quota = models.PositiveSmallIntegerField(
|
||
default=15, help_text='سقفِ سوالِ روزانه از دستیارِ هوش مصنوعی برای مشترکانِ این پلن.')
|
||
|
||
def __str__(self):
|
||
return self.title
|
||
|
||
|
||
class Subscription(models.Model):
|
||
"""وضعیتِ pending برای فاصلهی بینِ ساختنِ Subscription (در purchase) و
|
||
تاییدِ پرداخت (webhook)؛ فعالسازیِ واقعی توسطِ
|
||
subscription.services.activate_subscription انجام میشه."""
|
||
STATUS_CHOICES = [
|
||
('pending', 'در انتظار پرداخت'),
|
||
('active', 'فعال'),
|
||
('expired', 'منقضی'),
|
||
('canceled', 'لغوشده'),
|
||
]
|
||
|
||
user_id = models.IntegerField(unique=True, db_index=True) # ارجاع سبک به accounts.User
|
||
plan = models.ForeignKey(Plan, on_delete=models.SET_NULL, null=True, related_name='subscriptions')
|
||
status = models.CharField(max_length=15, choices=STATUS_CHOICES, default='pending')
|
||
started_at = models.DateTimeField(auto_now_add=True)
|
||
expires_at = models.DateTimeField(null=True, blank=True)
|
||
order_id = models.IntegerField(null=True, blank=True) # ارجاع سبک به payment.Order
|
||
# چون Subscription فقط یک ردیف در ازای هر user_id داره، دو خریدِ همزمانِ
|
||
# همون کاربر میتونن به وضعیتِ میانیِ کاملاً یکسانی برسن (مقایسهی
|
||
# فیلدها کافی نیست برای CAS). version با UPDATEِ اتمیکِ
|
||
# version=F('version')+1 (بدونِ select_for_update) هر نوشتنِ واقعی رو
|
||
# از نوشتنِ همزمانِ دیگری تمیز میده.
|
||
version = models.PositiveIntegerField(default=0)
|
||
|
||
def __str__(self):
|
||
return f"sub-user{self.user_id}-{self.status}"
|