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

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

View File

@@ -552,13 +552,13 @@ class YearlyPlanSeedTests(TestCase):
def test_the_yearly_plan_exists_after_migrations(self):
plan = Plan.objects.get(title='اشتراک یک‌ساله')
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.assertTrue(plan.features)
def test_a_monthly_plan_is_created_next_to_it(self):
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):
import importlib
@@ -566,6 +566,7 @@ class YearlyPlanSeedTests(TestCase):
reprice = importlib.import_module('subscription.migrations.0007_plans_priced_by_ai_cost')
Plan.objects.filter(title='اشتراک یک‌ساله').update(price=450_000, ai_daily_quota=9)
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='اشتراک یک‌ساله')
self.assertEqual((plan.price, plan.ai_daily_quota), (450_000, 9))
self.assertEqual(Plan.objects.filter(duration_days=30).count(), 1)
@@ -575,7 +576,7 @@ class YearlyPlanSeedTests(TestCase):
cache.clear()
data = self.client.get('/api/subscription/plans').json()
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):
from django.core.cache import cache