Paywall: first unit of each grade free; 1/3/6-month subscriptions; AI sold separately

- content: unit_is_free/has_unit_access; questions, node quiz, answer and lesson/node
  complete return 402 subscription_required outside the first unit without an active
  subscription. /api/home units carry requiresSubscription.
- subscription.0013: monthly 498k, quarterly 798k, six-month 1,298k toman, unlimited
  hearts, 3 AI questions/day; packs stay on sale for more questions.
- AI_DAILY_QUOTA_FREE default 3 -> 2.
- /api/subscription/status adds planTitle, planDurationDays, aiDailyQuota.
- tests: content/tests_paywall.py; existing unlock-order tests get a subscription.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 01:19:02 +03:30
parent 7d81d193bd
commit 64c4c09326
16 changed files with 214 additions and 28 deletions

View File

@@ -5,6 +5,7 @@
(با correctCount=0)؛ نقشه هم وضعیتِ «باز» را از روی درس‌های تمام‌شده حساب می‌کرد، پس تمام‌کردنِ
درسِ جزیره‌ی قفل آن را باز می‌کرد. فقط قلبِ صفر جلوی بی‌جواب‌تمام‌کردن را می‌گرفت.
"""
from subscription.models import Subscription
from django.core.cache import cache
from django.test import TestCase, override_settings
from rest_framework.test import APIClient
@@ -127,6 +128,9 @@ class FinishWithoutAnswersTests(_World, TestCase):
def test_the_normal_path_still_works_in_order(self):
"""جزیره‌ی اول، بعد دوم، بعد سوم؛ و اولین جزیره‌ی فصلِ دیگر از اول باز است."""
# فصلِ دوم اشتراک می‌خواهد (content.views.unit_is_free)؛ این تست ترتیبِ باز شدن را می‌سنجد، نه پرداخت.
Subscription.objects.create(user_id=self.user.id, status='active', expires_at=None)
self.assertEqual(self.statuses(), {'یک': 'unlocked', 'دو': 'locked', 'سه': 'locked', 'الف': 'unlocked'})
for node in (self.n1, self.n2, self.n3, self.m1):
record_answers(self.user.id, node=node)