@@ -151,7 +151,8 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): @property def balance(self): - return self.payment_plan.current_token_balance + pp = self.payment_plan + return pp.current_token_balance + pp.referral_balance @property def account_type(self): @@ -89,7 +89,7 @@ PATH_PREFETCH_MAP = { ), ), }, - '/api/v2/payments/plans': { + '/api/v1/v2/payments/plans': { 'select': ( 'host_account', 'payment_plan', @@ -1146,6 +1146,10 @@ msgstr "Платежный метод" msgid "Current balance" msgstr "Текущий баланс" +#: payments/models/payment_plan.py:75 +msgid "Referral balance" +msgstr "Реферальный баланс" + #: payments/models/payment_plan.py:90 payments/models/payment_plan.py:91 msgid "User Balance" msgstr "Баланс пользователя" @@ -0,0 +1,18 @@ +# Generated by Django 5.0.11 on 2026-04-18 06:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('payments', '0027_paymentmethod_remove_paymentplan_duration_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='paymentplanuserinfo', + name='referral_balance', + field=models.DecimalField(decimal_places=10, default=0, max_digits=100, verbose_name='Referral balance'), + ), + ] @@ -68,6 +68,12 @@ class PaymentPlanUserInfo(BaseModel): current_token_balance = models.DecimalField( max_digits=100, decimal_places=10, verbose_name=_('Current balance') ) + referral_balance = models.DecimalField( + max_digits=100, + decimal_places=10, + default=0, + verbose_name=_('Referral balance'), + ) def save( self, @@ -126,8 +126,9 @@ class PromoCodeActivation(BaseModel): def add_tokens_referral(self, amount: int): self.add_tokens(amount=amount) - self.promocode.owner.payment_plan.current_token_balance += amount - self.promocode.owner.payment_plan.save() + owner_pp = self.promocode.owner.payment_plan + owner_pp.referral_balance += amount + owner_pp.save(update_fields=['referral_balance']) def __str__(self) -> str: return f'{self.promocode}' @@ -18,15 +18,17 @@ class PaymentPlanSelector: if ( self.user.account_type in ('business_account', 'business_admin', 'business_security') ) and self.user.business_account.acceptance_status == InvitationStatus.ACCEPTED: - balance = ( + pp = self.user.business_account.parent_company.user.payment_plan + total_available = pp.current_token_balance + pp.referral_balance + limit = ( self.user.business_account.group.token_limit if self.user.business_account.group else self.user.business_account.token_limit ) - if balance is None: - balance = self.user.business_account.parent_company.user.payment_plan.current_token_balance + balance = min(total_available, limit) if limit is not None else total_available else: - balance = self.user.payment_plan.current_token_balance + pp = self.user.payment_plan + balance = pp.current_token_balance + pp.referral_balance return balance @@ -30,13 +30,20 @@ class ModelBillingService: else: raise Exception(_('Unknown account type')) - if plan.current_token_balance < amount: - raise InsufficientBalance(plan.current_token_balance, amount) + total_available = plan.current_token_balance + plan.referral_balance + if total_available < amount: + raise InsufficientBalance(total_available, amount) - if (allowance is not None) and (allowance < amount): + if allowance is not None and allowance < amount: raise InsufficientBalance(allowance, amount) - plan.current_token_balance -= amount + remainder = amount + from_main = min(plan.current_token_balance, remainder) + plan.current_token_balance -= from_main + remainder -= from_main + if remainder > 0: + plan.referral_balance -= remainder + if allowance is not None: if self.user.business_account.group is not None: self.user.business_account.group.token_limit -= amount @@ -32,8 +32,9 @@ class ReferralAccountService: payment=payment, amount=accrual_amount, ) - referer_account.owner.payment_plan.current_token_balance += accrual_amount - referer_account.owner.payment_plan.save() + pp = referer_account.owner.payment_plan + pp.referral_balance += accrual_amount + pp.save(update_fields=['referral_balance']) logger.info( 'Referral accrual applied: referer_email=%s invitee_email=%s amount=%s', referer_account.owner.email, @@ -129,10 +129,11 @@ class PlansAPITest(BaseAuthorizedAPITest): 'uid', 'price', 'tokens_per_plan', + 'is_corporate', 'points', 'grouped_features', 'accessed_models', - 'individual' + 'individual', ], ) @@ -46,9 +46,21 @@ class BalanceAPITest(BaseAuthorizedAPITest): balance = self.get().json()['current_token_balance'] self.assertEqual(Decimal(balance), Decimal('8.00')) + def test_balance_includes_referral(self) -> None: + pp = self.user.payment_plan + pp.current_token_balance = Decimal('3') + pp.referral_balance = Decimal('7') + pp.save() + balance = self.get().json()['current_token_balance'] + self.assertEqual(Decimal(balance), Decimal('10')) + def test_displaying_balance(self) -> None: balance = self.get().json()['current_token_balance'] self.assertEqual(Decimal(balance), Decimal('10.00')) + hp = self.host_user.payment_plan + hp.current_token_balance = Decimal('900') + hp.referral_balance = Decimal('0') + hp.save() business_account = BusinessAccount.objects.create( user=self.user, parent_company=self.host, acceptance_status='accepted', token_limit=Decimal('50') ) @@ -56,7 +56,7 @@ class PaymentPlanAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): @admin.register(PaymentPlanUserInfo) class PaymentPlanUserInfoAdmin(admin.ModelAdmin): - list_display = ['user', 'current_token_balance', 'plan', 'updated_at', 'next_payment_at'] + list_display = ['user', 'current_token_balance', 'referral_balance', 'plan', 'updated_at', 'next_payment_at'] raw_id_fields = ['user'] search_fields = [ @@ -23,7 +23,8 @@ logger = get_task_logger(__name__) def send_low_balance_message(): hosts = BusinessUserHost.objects.filter( token_cap_enabled=True, - token_cap__gt=F('user__payment_plan__current_token_balance'), + token_cap__gt=F('user__payment_plan__current_token_balance') + + F('user__payment_plan__referral_balance'), ) for host in hosts: EmailService.send_low_balance_email(host)