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:
@@ -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 میمانند؛ دموها رد میشوند. «ورود با حسابِ دیگر» فقط خروج است؛
|
||||||
چند حساب روی یک دستگاه عمداً نگه داشته نمیشود.
|
چند حساب روی یک دستگاه عمداً نگه داشته نمیشود.
|
||||||
|
|||||||
@@ -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` میدهد و کارتِ پلن عدد را از همین میسازد.
|
||||||
- انتخابِ عددها تصمیمِ محصولی است. جدولِ قیمت در برابرِ سقف در نسخهی کاملِ گزارش آمده است.
|
- انتخابِ عددها تصمیمِ محصولی است. جدولِ قیمت در برابرِ سقف در نسخهی کاملِ گزارش آمده است.
|
||||||
|
|
||||||
|
|||||||
42
subscription/migrations/0008_paid_quota_ten_a_day.py
Normal file
42
subscription/migrations/0008_paid_quota_ten_a_day.py
Normal 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),
|
||||||
|
]
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user