public user IDs, plans priced by tutor cost, age-based tutor tone, self-service account deletion

- profile publicId ("User482917", accounts/public_id.py); admin shows and searches it
- plans API reports aiDailyQuota per plan and freeAiDailyQuota; migration 0007 sets the
  temporary prices (monthly 99,000 / yearly 990,000 toman, 5 tutor questions a day) unless
  an admin already edited the yearly plan
- tutor tone follows the learner's age (18+ gets an adult tone at any grade); notation
  still follows the grade's book; answer cache keyed by tone (prompt tutor-v12-age-tone)
- POST /api/user/delete: phone users confirm with a fresh SMS code (same checks as login,
  now shared in verify_phone_otp), guests type "حذف"; payment records are kept, demo
  accounts refused
- delete_users --skip-paid

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 06:10:17 +03:30
parent e9d676ce68
commit 63cd1af6f7
18 changed files with 468 additions and 78 deletions

View File

@@ -547,15 +547,36 @@ class PlanCardContractTests(TestCase):
class YearlyPlanSeedTests(TestCase):
"""migrationِ 0005: سرورِ بی‌پلن یک پلنِ یک‌ساله می‌گیرد."""
"""migrationهای 0005 و 0007: سرورِ بی‌پلن پلنِ سالانه و ماهانه با قیمتِ ۱۴۰۵-۰۷ می‌گیرد."""
def test_the_yearly_plan_exists_after_migrations(self):
plan = Plan.objects.get(title='اشتراک یک‌ساله')
self.assertEqual(plan.duration_days, 365)
self.assertEqual(plan.price, 299_000)
self.assertEqual((plan.price, plan.ai_daily_quota), (990_000, 5))
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))
def test_repricing_leaves_an_admin_edited_yearly_plan_alone(self):
import importlib
from django.apps import apps as global_apps
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)
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)
def test_plans_api_reports_each_quota_and_the_free_one(self):
from django.core.cache import cache
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})
def test_it_is_served_to_the_app(self):
from django.core.cache import cache
cache.clear()