- 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>
86 lines
4.2 KiB
Python
86 lines
4.2 KiB
Python
"""
|
||
اپ Notifications برخلاف بقیهی اپها به هیچ اپی رفرنس سبک نمیده و خودش
|
||
تولیدکنندهی رویداد نیست؛ فقط event-listenerه (طبق سند معماری داده).
|
||
چون این پروژه یک مونولیت جنگوییه، «گوش دادن به رویداد» با یک تابع کمکی
|
||
پیادهسازی شده که سایر اپها (Progress، Payment، Support) مستقیماً
|
||
از این ماژول import و صدا میزنن — دقیقاً همون الگوی call_payment_gateway
|
||
در payment/services.py و create_order که سرور-به-سرور از subscription
|
||
صدا زده میشه.
|
||
|
||
⚙️ افزوده شد: پارامترِ channel (فقط 'in_app' یا 'sms'، پیشفرض 'in_app')
|
||
تا notify() علاوهبر ثبتِ Notificationِ داخلاپ، بتونه مستقیماً پیامک هم
|
||
بفرسته. برای sms هیچ منطقِ provider تکرار نشده — دقیقاً همون
|
||
accounts.services.send_sms که campaigns.services هم برای ارسالِ کمپینِ
|
||
پیامکی ازش استفاده میکنه، اینجا هم صدا زده میشه. این اپ همچنان به
|
||
accounts.models رفرنسی نداره (طبق پاراگرافِ بالا)؛ به همین دلیل شمارهتلفن
|
||
از خودِ notify() استخراج نمیشه و caller (که از قبل User رو در اختیار
|
||
داره) باید phone رو صریحاً پاس بده.
|
||
"""
|
||
from accounts.services import send_sms
|
||
|
||
from .models import Notification
|
||
|
||
CHANNEL_IN_APP = 'in_app'
|
||
CHANNEL_SMS = 'sms'
|
||
CHANNEL_CHOICES = {CHANNEL_IN_APP, CHANNEL_SMS}
|
||
|
||
|
||
def notify(
|
||
*,
|
||
user_id: int,
|
||
type: str,
|
||
title: str,
|
||
body: str = '',
|
||
related_id: int | None = None,
|
||
channel: str = CHANNEL_IN_APP,
|
||
phone: str | None = None,
|
||
) -> Notification | None:
|
||
"""
|
||
ثبت یک اعلانِ جدید برای کاربر (channel='in_app'، پیشفرض و بدونِ هیچ
|
||
تغییری در رفتارِ قبلی) یا ارسالِ همون پیام بهصورتِ پیامک
|
||
(channel='sms').
|
||
|
||
نمونهی استفادهی in_app (بدون تغییر نسبت به قبل):
|
||
|
||
from notifications.services import notify
|
||
notify(user_id=order.user_id, type='order_paid',
|
||
title='پرداخت با موفقیت انجام شد', related_id=order.id)
|
||
|
||
نمونهی استفادهی sms:
|
||
|
||
notify(user_id=user.id, type='order_paid', title='...',
|
||
body='سفارش شما با موفقیت پرداخت شد.',
|
||
channel='sms', phone=user.phone)
|
||
|
||
برای channel='sms':
|
||
- phone الزامیه؛ بدونِ اون ValueError میده (نه یک ارسالِ خاموش/ناقص).
|
||
- هیچ رکوردِ Notification ساخته نمیشه — پیامک مستقیماً از طریقِ
|
||
accounts.services.send_sms ارسال میشه، پس مقدارِ برگشتی Noneه.
|
||
- متنِ پیامک، body است؛ اگه body خالی باشه از title استفاده میشه (تا
|
||
caller مجبور به تکرارِ متن در هر دو فیلد نباشه).
|
||
- SMSDeliveryError (که send_sms در صورتِ شکستِ ارسال raise میکنه)
|
||
عمداً اینجا catch نمیشه؛ caller باید تصمیم بگیره با شکستِ ارسال چه
|
||
کار کنه — دقیقاً همالگو با اینکه send_sms خودش هیچ حالتِ سکوت/
|
||
best-effort نداره.
|
||
|
||
مقدارِ نامعتبر برای channel (نه in_app نه sms) با ValueError رد
|
||
میشه، نه با یک ارسالِ خاموش/پیشفرض.
|
||
"""
|
||
if channel not in CHANNEL_CHOICES:
|
||
raise ValueError(f"Unsupported notification channel: {channel!r}")
|
||
|
||
if channel == CHANNEL_SMS:
|
||
if not phone:
|
||
raise ValueError("phone is required when channel='sms'")
|
||
send_sms(phone=phone, message=body or title)
|
||
return None
|
||
|
||
return Notification.objects.create(
|
||
user_id=user_id,
|
||
type=type,
|
||
title=title,
|
||
body=body,
|
||
related_id=related_id,
|
||
)
|
||
|