fix heart refill and grant unlimited hearts to active subscribers
next_refill_at was set on UserHearts but never read or recalculated, so a heart lost to zero never came back. UserHearts.apply_regen() now catches up missed 24h intervals on read/deduct, and lose_heart() starts the timer only when it isn't already running. Any user with an active (non-expired) subscription now skips heart deduction entirely, regardless of which plan they're on — good enough for now until per-plan perks are needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework.throttling import ScopedRateThrottle
|
||||
|
||||
from accounts.authentication import issue_access_token
|
||||
from accounts.models import User
|
||||
from progress.models import UserHearts, UserStats
|
||||
from subscription.models import Plan, Subscription
|
||||
|
||||
from .models import Choice, Grade, Lesson, Node, NodeProgress, Question, Unit, UserAnswer, UserLessonCompletion
|
||||
|
||||
@@ -636,6 +639,63 @@ class GradesListCacheTests(TestCase):
|
||||
self.assertEqual(len(r3.data['grades']), 2)
|
||||
|
||||
|
||||
class UnlimitedHeartsSubscriptionTests(TestCase):
|
||||
"""رگرسیون: هر مشترکِ فعال (فارغ از پلن) نباید با جوابِ غلط هارت از دست بده."""
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.user = User.objects.create(device_id='dev-unlimited-hearts', is_guest=True, name='')
|
||||
self.token = issue_access_token(self.user.id)
|
||||
|
||||
grade = Grade.objects.create(title='پایه اول', icon='icon', color='#fff')
|
||||
unit = Unit.objects.create(grade=grade, title='فصل ۱', order=1)
|
||||
node = Node.objects.create(unit=unit, title='گره ۱', order=1)
|
||||
lesson = Lesson.objects.create(node=node, title='درس ۱', intro_text='...')
|
||||
self.question = Question.objects.create(lesson=lesson, type='multiple_choice', prompt='سوال ۱')
|
||||
Choice.objects.create(question=self.question, value='درست', is_correct=True)
|
||||
self.wrong_choice = Choice.objects.create(question=self.question, value='غلط', is_correct=False)
|
||||
|
||||
def _answer_wrong(self):
|
||||
return self.client.post(
|
||||
f'/api/questions/{self.question.id}/answer',
|
||||
{'selectedOptionId': str(self.wrong_choice.id)},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'Bearer {self.token}',
|
||||
)
|
||||
|
||||
def test_wrong_answer_costs_a_heart_without_subscription(self):
|
||||
UserHearts.objects.create(user_id=self.user.id, current_hearts=5, max_hearts=5)
|
||||
|
||||
response = self._answer_wrong()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['heartsRemaining'], 4)
|
||||
|
||||
def test_wrong_answer_does_not_cost_a_heart_with_active_subscription(self):
|
||||
UserHearts.objects.create(user_id=self.user.id, current_hearts=5, max_hearts=5)
|
||||
plan = Plan.objects.create(title='ماهانه', price=100000)
|
||||
Subscription.objects.create(
|
||||
user_id=self.user.id, plan=plan, status='active',
|
||||
expires_at=timezone.now() + timedelta(days=30),
|
||||
)
|
||||
|
||||
response = self._answer_wrong()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['heartsRemaining'], 5)
|
||||
self.assertEqual(UserHearts.objects.get(user_id=self.user.id).current_hearts, 5)
|
||||
|
||||
def test_expired_subscription_still_costs_a_heart(self):
|
||||
UserHearts.objects.create(user_id=self.user.id, current_hearts=5, max_hearts=5)
|
||||
plan = Plan.objects.create(title='ماهانه', price=100000)
|
||||
Subscription.objects.create(
|
||||
user_id=self.user.id, plan=plan, status='active',
|
||||
expires_at=timezone.now() - timedelta(days=1),
|
||||
)
|
||||
|
||||
response = self._answer_wrong()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['heartsRemaining'], 4)
|
||||
|
||||
|
||||
class ReadThrottleTests(TestCase):
|
||||
def setUp(self):
|
||||
_django_cache.clear()
|
||||
|
||||
Reference in New Issue
Block a user