paid plans: 10 tutor questions a day at 179,000 / 1,790,000 toman

Five a day was too close to the free three. Pricing keeps the tutor under ~40% of net revenue
at typical use (a third of the cap); full numbers in the cost report and docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 10:11:31 +03:30
parent b3940e87f7
commit 2edc56fc08
5 changed files with 54 additions and 6 deletions

View File

@@ -409,7 +409,8 @@ anyone who can read this repo can forge a token for any user.**
- **لحنِ دستیار از سن** (`tone_for`، پرامپت `tutor-v12-age-tone`): ۱۸+ در هر پایه لحنِ بزرگسال می‌گیرد؛ - **لحنِ دستیار از سن** (`tone_for`، پرامپت `tutor-v12-age-tone`): ۱۸+ در هر پایه لحنِ بزرگسال می‌گیرد؛
نمادگذاری از کتابِ پایه. کشِ جواب با لحن کلید می‌خورد. فقط یک مقایسه‌ی زنده (سوم، ۸ و ۴۵ سال). نمادگذاری از کتابِ پایه. کشِ جواب با لحن کلید می‌خورد. فقط یک مقایسه‌ی زنده (سوم، ۸ و ۴۵ سال).
- **شناسه‌ی نمایشی** `User482917` (`accounts/public_id.py`، از id، بی‌ستون؛ ادمین با آن جستجو می‌کند). - **شناسه‌ی نمایشی** `User482917` (`accounts/public_id.py`، از id، بی‌ستون؛ ادمین با آن جستجو می‌کند).
- **قیمتِ موقتِ پلن‌ها** (`subscription.0007`): ماهانه ۹۹ و سالانه ۹۹۰ هزار تومان، هر دو ۵ سوال در روز. - **قیمتِ پلن‌ها** (`subscription.0008`): ماهانه ۱۷۹ و سالانه ۱٬۷۹۰ هزار تومان، هر دو ۱۰ سوال در روز
(۰۰۰۷ ۹۹ هزار و ۵ سوال بود؛ ۵ در برابرِ ۳ِ رایگان بی‌فایده بود). حساب در گزارشِ هزینه‌ی گزارش-۲۶.
- **حذفِ حساب** `POST /api/user/delete`: شماره‌دار با کدِ پیامکیِ تازه (`verify_phone_otp`، مشترک با ورود)، - **حذفِ حساب** `POST /api/user/delete`: شماره‌دار با کدِ پیامکیِ تازه (`verify_phone_otp`، مشترک با ورود)،
مهمان با «حذف»؛ سفارش‌ها و statuslog می‌مانند؛ دموها رد می‌شوند. «ورود با حسابِ دیگر» فقط خروج است؛ مهمان با «حذف»؛ سفارش‌ها و statuslog می‌مانند؛ دموها رد می‌شوند. «ورود با حسابِ دیگر» فقط خروج است؛
چند حساب روی یک دستگاه عمداً نگه داشته نمی‌شود. چند حساب روی یک دستگاه عمداً نگه داشته نمی‌شود.

View File

@@ -39,7 +39,11 @@ Message.objects.filter(role='assistant', created_at__gte=since).values('conversa
می‌گیرد. کد در `subscription.services.ai_daily_quota` است. می‌گیرد. کد در `subscription.services.ai_daily_quota` است.
- کاربرِ رایگانی که به سقف می‌رسد پیامِ «با خریدِ اشتراک…» می‌گیرد و اپ دکمه‌ی «دیدنِ اشتراک‌ها» - کاربرِ رایگانی که به سقف می‌رسد پیامِ «با خریدِ اشتراک…» می‌گیرد و اپ دکمه‌ی «دیدنِ اشتراک‌ها»
را نشان می‌دهد. را نشان می‌دهد.
- قیمتِ موقت (migrationِ `subscription.0007`): ماهانه ۹۹٬۰۰۰ و سالانه ۹۹۰٬۰۰۰ تومان، هر دو ۵ سوال در روز. - قیمتِ فعلی (migrationِ `subscription.0008`): ماهانه ۱۷۹٬۰۰۰ و سالانه ۱٬۷۹۰٬۰۰۰ تومان، هر دو ۱۰ سوال در
روز. ۵ سوال (`0007`) در برابرِ ۳ِ رایگان تفاوتی حس نمی‌شد. با هزینه‌ی ~۶۰۰ تومان برای هر سوال و مصرفِ
یک‌سومِ سقف، ۱۰ سوال حدودِ ~۱۷۶ هزار تومان در ماه می‌خواهد تا دستیار حداکثر ۴۰٪ِ درآمدِ خالص (پس از ۱۵٪یِ
بازار) باشد؛ ۱۵ سوال ~۲۶۵ هزار. خرجِ کاربرهای رایگان به ازای هر مشترک با درصدِ تبدیل تغییر می‌کند: با ۵٪
تبدیل و میانگینِ ۵ سوال در ماه برای هر رایگان، ~۵۷ هزار تومان؛ زیرِ ۵٪ از خرجِ خودِ مشترک بیشتر می‌شود.
API پلن‌ها `aiDailyQuota` و `freeAiDailyQuota` می‌دهد و کارتِ پلن عدد را از همین می‌سازد. API پلن‌ها `aiDailyQuota` و `freeAiDailyQuota` می‌دهد و کارتِ پلن عدد را از همین می‌سازد.
- انتخابِ عددها تصمیمِ محصولی است. جدولِ قیمت در برابرِ سقف در نسخه‌ی کاملِ گزارش آمده است. - انتخابِ عددها تصمیمِ محصولی است. جدولِ قیمت در برابرِ سقف در نسخه‌ی کاملِ گزارش آمده است.

View File

@@ -0,0 +1,42 @@
"""
سقفِ پولی ۱۰ سوال در روز، با قیمتی که هزینه‌اش را می‌پوشاند (۲۰۲۶-۱۰-۰۲).
۵ سوال در روز (0007) در برابرِ ۳ِ رایگان تفاوتی حس نمی‌شد (صاحبِ پروژه). با ۱۰ سوال و هزینه‌ی
میانگینِ ~۶۰۰ تومان برای هر سوال، مشترکی که یک‌سومِ سقف را مصرف کند (~۱۰۰ سوال در ماه) حدودِ
۶۰ هزار تومان هزینه دارد. ماهانه‌ی ۱۷۹ هزار تومانی آن را زیرِ ۴۰٪ِ درآمدِ خالص نگه می‌دارد (۳۹٪ با
سهمِ ۱۵٪یِ بازار، ۳۴٪ با زرین‌پال)؛ سالانه‌ی ۱٬۷۹۰ هزار تومانی (قیمتِ ۱۰ ماه) ۴۷٪ / ۴۰٪. مشترکی که
هر روز سقف را پر کند (۳۰۰ سوال، ~۱۸۰ هزار تومان) زیان دارد؛ حسابِ کامل: گزارشِ هزینه در
گزارش-۲۶ و docs/ai-cost-and-guardrails.md.
فقط پلن‌هایی را عوض می‌کند که هنوز همان قیمت و سقفِ 0007 را دارند، تا ویرایشِ ادمین بازنویسی نشود.
"""
from django.db import migrations
CHANGES = [
# (قیمتِ 0007، مدت) → مقدارِ تازه
((99_000, 30), {'price': 179_000, 'ai_daily_quota': 10}),
((990_000, 365), {'price': 1_790_000, 'ai_daily_quota': 10, 'discount_percent': 17}),
]
def apply(apps, schema_editor):
Plan = apps.get_model('subscription', 'Plan')
for (price, days), new in CHANGES:
Plan.objects.filter(price=price, duration_days=days, ai_daily_quota=5).update(**new)
def undo(apps, schema_editor):
Plan = apps.get_model('subscription', 'Plan')
for (price, days), new in CHANGES:
Plan.objects.filter(price=new['price'], duration_days=days).update(price=price, ai_daily_quota=5)
class Migration(migrations.Migration):
dependencies = [
('subscription', '0007_plans_priced_by_ai_cost'),
]
operations = [
migrations.RunPython(apply, undo),
]

View File

@@ -26,7 +26,7 @@ class Plan(models.Model):
# برچسبِ بالای کارت: «محبوب‌ترین»، «بهترین ارزش». `popular` فقط بولین است # برچسبِ بالای کارت: «محبوب‌ترین»، «بهترین ارزش». `popular` فقط بولین است
# و یک متن نمی‌دهد؛ این جایگزینش نیست، کنارش است. # و یک متن نمی‌دهد؛ این جایگزینش نیست، کنارش است.
badge = models.CharField(max_length=30, blank=True) badge = models.CharField(max_length=30, blank=True)
# چند سوال در روز از دستیار. هر سوال حدودِ ۰٫۲ سنتِ دلار (~۶۰۰ تومان) هزینه‌ی API # چند سوال در روز از دستیار (۱۰ برای پلن‌های فعلی، subscription.0008). هر سوال حدودِ ۰٫۲ سنتِ دلار (~۶۰۰ تومان) هزینه‌ی API
# دارد (docs/ai-cost-and-guardrails.md)، پس این عدد مستقیم سودِ پلن را تعیین می‌کند. # دارد (docs/ai-cost-and-guardrails.md)، پس این عدد مستقیم سودِ پلن را تعیین می‌کند.
# کاربرِ بدونِ اشتراک سقفِ AI_DAILY_QUOTA_FREE (settings) را دارد. # کاربرِ بدونِ اشتراک سقفِ AI_DAILY_QUOTA_FREE (settings) را دارد.
ai_daily_quota = models.PositiveSmallIntegerField( ai_daily_quota = models.PositiveSmallIntegerField(

View File

@@ -552,13 +552,13 @@ class YearlyPlanSeedTests(TestCase):
def test_the_yearly_plan_exists_after_migrations(self): def test_the_yearly_plan_exists_after_migrations(self):
plan = Plan.objects.get(title='اشتراک یک‌ساله') plan = Plan.objects.get(title='اشتراک یک‌ساله')
self.assertEqual(plan.duration_days, 365) self.assertEqual(plan.duration_days, 365)
self.assertEqual((plan.price, plan.ai_daily_quota), (990_000, 5)) self.assertEqual((plan.price, plan.ai_daily_quota), (1_790_000, 10))
self.assertEqual(plan.currency, 'IRT') self.assertEqual(plan.currency, 'IRT')
self.assertTrue(plan.features) self.assertTrue(plan.features)
def test_a_monthly_plan_is_created_next_to_it(self): def test_a_monthly_plan_is_created_next_to_it(self):
plan = Plan.objects.get(duration_days=30) plan = Plan.objects.get(duration_days=30)
self.assertEqual((plan.price, plan.ai_daily_quota), (99_000, 5)) self.assertEqual((plan.price, plan.ai_daily_quota), (179_000, 10))
def test_repricing_leaves_an_admin_edited_yearly_plan_alone(self): def test_repricing_leaves_an_admin_edited_yearly_plan_alone(self):
import importlib import importlib
@@ -566,6 +566,7 @@ class YearlyPlanSeedTests(TestCase):
reprice = importlib.import_module('subscription.migrations.0007_plans_priced_by_ai_cost') reprice = importlib.import_module('subscription.migrations.0007_plans_priced_by_ai_cost')
Plan.objects.filter(title='اشتراک یک‌ساله').update(price=450_000, ai_daily_quota=9) Plan.objects.filter(title='اشتراک یک‌ساله').update(price=450_000, ai_daily_quota=9)
reprice.apply_prices(global_apps, None) reprice.apply_prices(global_apps, None)
importlib.import_module('subscription.migrations.0008_paid_quota_ten_a_day').apply(global_apps, None)
plan = Plan.objects.get(title='اشتراک یک‌ساله') plan = Plan.objects.get(title='اشتراک یک‌ساله')
self.assertEqual((plan.price, plan.ai_daily_quota), (450_000, 9)) self.assertEqual((plan.price, plan.ai_daily_quota), (450_000, 9))
self.assertEqual(Plan.objects.filter(duration_days=30).count(), 1) self.assertEqual(Plan.objects.filter(duration_days=30).count(), 1)
@@ -575,7 +576,7 @@ class YearlyPlanSeedTests(TestCase):
cache.clear() cache.clear()
data = self.client.get('/api/subscription/plans').json() data = self.client.get('/api/subscription/plans').json()
self.assertEqual(data['freeAiDailyQuota'], 3) self.assertEqual(data['freeAiDailyQuota'], 3)
self.assertEqual({p['aiDailyQuota'] for p in data['plans']}, {5}) self.assertEqual({p['aiDailyQuota'] for p in data['plans']}, {10})
def test_it_is_served_to_the_app(self): def test_it_is_served_to_the_app(self):
from django.core.cache import cache from django.core.cache import cache