From e1bcb1ba910ef3a79dec5fa974ae8e6c09494aa7 Mon Sep 17 00:00:00 2001 From: Fringg Date: Mon, 16 Mar 2026 03:57:50 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D1=80=D0=B5=D1=84=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=B1=D0=BE=D0=BD=D1=83=D1=81?= =?UTF-8?q?=20=D0=B8=D0=BD=D0=B2=D0=B0=D0=B9=D1=82=D0=B5=D1=80=D0=B0=20?= =?UTF-8?q?=E2=80=94=20=D1=81=D1=83=D0=BC=D0=BC=D0=B0=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20=D0=BC=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=D1=83?= =?UTF-8?q?=D0=BC=D0=B0,=20=D0=B7=D0=B0=D1=89=D0=B8=D1=82=D0=B0=20=D1=84?= =?UTF-8?q?=D0=BB=D0=B0=D0=B3=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=BF=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - referral_service: inviter_bonus = fixed + commission вместо max(fixed, commission) - 13 платёжных провайдеров: has_made_first_topup ставится только для нереферальных юзеров - riopay: критический фикс — флаг ставился до вызова referral_service - Обновлены уведомления с разбивкой бонуса - Исправлен и дополнен тест referral_service --- app/services/payment/cloudpayments.py | 2 +- app/services/payment/cryptobot.py | 2 +- app/services/payment/freekassa.py | 2 +- app/services/payment/heleket.py | 2 +- app/services/payment/kassa_ai.py | 2 +- app/services/payment/mulenpay.py | 2 +- app/services/payment/pal24.py | 2 +- app/services/payment/platega.py | 2 +- app/services/payment/riopay.py | 2 +- app/services/payment/stars.py | 2 +- app/services/payment/wata.py | 2 +- app/services/payment/yookassa.py | 2 +- app/services/referral_service.py | 22 +++++++--- app/services/tribute_service.py | 2 +- tests/services/test_referral_service.py | 58 ++++++++++++++++++++++++- 15 files changed, 86 insertions(+), 20 deletions(-) diff --git a/app/services/payment/cloudpayments.py b/app/services/payment/cloudpayments.py index cab418dd..3248e2c4 100644 --- a/app/services/payment/cloudpayments.py +++ b/app/services/payment/cloudpayments.py @@ -347,7 +347,7 @@ class CloudPaymentsPaymentMixin: except Exception as error: logger.error('Ошибка обработки реферального пополнения CloudPayments', error=error) - if was_first_topup and not user.has_made_first_topup: + if was_first_topup and not user.has_made_first_topup and not user.referred_by_id: user.has_made_first_topup = True await db.commit() await db.refresh(user) diff --git a/app/services/payment/cryptobot.py b/app/services/payment/cryptobot.py index b0cbf4cd..be8bacf7 100644 --- a/app/services/payment/cryptobot.py +++ b/app/services/payment/cryptobot.py @@ -338,7 +338,7 @@ class CryptoBotPaymentMixin: except Exception as error: logger.error('Ошибка обработки реферального пополнения CryptoBot', error=error) - if was_first_topup and not user.has_made_first_topup: + if was_first_topup and not user.has_made_first_topup and not user.referred_by_id: user.has_made_first_topup = True await db.commit() diff --git a/app/services/payment/freekassa.py b/app/services/payment/freekassa.py index 066d3147..c49aca8e 100644 --- a/app/services/payment/freekassa.py +++ b/app/services/payment/freekassa.py @@ -347,7 +347,7 @@ class FreekassaPaymentMixin: except Exception as error: logger.error('Ошибка обработки реферального пополнения Freekassa', error=error) - if was_first_topup and not user.has_made_first_topup: + if was_first_topup and not user.has_made_first_topup and not user.referred_by_id: user.has_made_first_topup = True await db.commit() diff --git a/app/services/payment/heleket.py b/app/services/payment/heleket.py index e50c6c96..50c51a3a 100644 --- a/app/services/payment/heleket.py +++ b/app/services/payment/heleket.py @@ -391,7 +391,7 @@ class HeleketPaymentMixin: except Exception as error: # pragma: no cover - defensive logger.error('Ошибка реферального начисления Heleket', error=error) - if was_first_topup and not user.has_made_first_topup: + if was_first_topup and not user.has_made_first_topup and not user.referred_by_id: user.has_made_first_topup = True await db.commit() await db.refresh(user) diff --git a/app/services/payment/kassa_ai.py b/app/services/payment/kassa_ai.py index 3c97e086..9f7273c8 100644 --- a/app/services/payment/kassa_ai.py +++ b/app/services/payment/kassa_ai.py @@ -335,7 +335,7 @@ class KassaAiPaymentMixin: except Exception as error: logger.error('Ошибка обработки реферального пополнения KassaAI', error=error) - if was_first_topup and not user.has_made_first_topup: + if was_first_topup and not user.has_made_first_topup and not user.referred_by_id: user.has_made_first_topup = True await db.commit() diff --git a/app/services/payment/mulenpay.py b/app/services/payment/mulenpay.py index 1993ee1c..3cb241f7 100644 --- a/app/services/payment/mulenpay.py +++ b/app/services/payment/mulenpay.py @@ -322,7 +322,7 @@ class MulenPayPaymentMixin: except Exception as error: logger.error('Ошибка обработки реферального пополнения', display_name=display_name, error=error) - if was_first_topup and not user.has_made_first_topup: + if was_first_topup and not user.has_made_first_topup and not user.referred_by_id: user.has_made_first_topup = True await db.commit() diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py index a5582c6e..c9b721ca 100644 --- a/app/services/payment/pal24.py +++ b/app/services/payment/pal24.py @@ -434,7 +434,7 @@ class Pal24PaymentMixin: except Exception as error: logger.error('Ошибка обработки реферального пополнения Pal24', error=error) - if was_first_topup and not user.has_made_first_topup: + if was_first_topup and not user.has_made_first_topup and not user.referred_by_id: user.has_made_first_topup = True await db.commit() diff --git a/app/services/payment/platega.py b/app/services/payment/platega.py index 8a79992b..bdab8c46 100644 --- a/app/services/payment/platega.py +++ b/app/services/payment/platega.py @@ -442,7 +442,7 @@ class PlategaPaymentMixin: except Exception as error: logger.error('Ошибка обработки реферального пополнения Platega', error=error) - if was_first_topup and not user.has_made_first_topup: + if was_first_topup and not user.has_made_first_topup and not user.referred_by_id: user.has_made_first_topup = True await db.commit() await db.refresh(user) diff --git a/app/services/payment/riopay.py b/app/services/payment/riopay.py index b0fbcd66..c3684426 100644 --- a/app/services/payment/riopay.py +++ b/app/services/payment/riopay.py @@ -316,7 +316,7 @@ class RioPayPaymentMixin: UserModel.balance_kopeks: UserModel.balance_kopeks + payment.amount_kopeks, UserModel.updated_at: datetime.now(UTC), } - if was_first_topup: + if was_first_topup and not user.referred_by_id: update_values[UserModel.has_made_first_topup] = True await db.execute(update(UserModel).where(UserModel.id == user.id).values(update_values)) diff --git a/app/services/payment/stars.py b/app/services/payment/stars.py index b95cd8b7..e29c70ca 100644 --- a/app/services/payment/stars.py +++ b/app/services/payment/stars.py @@ -442,7 +442,7 @@ class TelegramStarsMixin: "❌ Описание '' не подходит для реферальной логики", description_for_referral=description_for_referral ) - if was_first_topup and not user.has_made_first_topup: + if was_first_topup and not user.has_made_first_topup and not user.referred_by_id: user.has_made_first_topup = True await db.commit() diff --git a/app/services/payment/wata.py b/app/services/payment/wata.py index 39fd3c07..21bb7f14 100644 --- a/app/services/payment/wata.py +++ b/app/services/payment/wata.py @@ -529,7 +529,7 @@ class WataPaymentMixin: except Exception as error: logger.error('Ошибка обработки реферального пополнения WATA', error=error) - if was_first_topup and not user.has_made_first_topup: + if was_first_topup and not user.has_made_first_topup and not user.referred_by_id: user.has_made_first_topup = True await db.commit() await db.refresh(user) diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py index 9405ecc8..e95b4285 100644 --- a/app/services/payment/yookassa.py +++ b/app/services/payment/yookassa.py @@ -819,7 +819,7 @@ class YooKassaPaymentMixin: except Exception as error: logger.error('Ошибка обработки реферального пополнения YooKassa', error=error) - if was_first_topup and not getattr(user, 'has_made_first_topup', False): + if was_first_topup and not getattr(user, 'has_made_first_topup', False) and not user.referred_by_id: user.has_made_first_topup = True await db.commit() diff --git a/app/services/referral_service.py b/app/services/referral_service.py index 7b816658..c28a1cbd 100644 --- a/app/services/referral_service.py +++ b/app/services/referral_service.py @@ -132,8 +132,8 @@ async def process_referral_registration(db: AsyncSession, new_user_id: int, refe ) if settings.REFERRAL_INVITER_BONUS_KOPEKS > 0: inviter_notification += ( - f'вы получите минимум {settings.format_price(settings.REFERRAL_INVITER_BONUS_KOPEKS)} или ' - f'{commission_percent}% от суммы (что больше).\n\n' + f'вы получите {settings.format_price(settings.REFERRAL_INVITER_BONUS_KOPEKS)} + ' + f'{commission_percent}% от суммы пополнения.\n\n' ) else: inviter_notification += f'вы получите {commission_percent}% от суммы.\n\n' @@ -304,7 +304,7 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko ) commission_amount = int(topup_amount_kopeks * commission_percent / 100) - inviter_bonus = max(settings.REFERRAL_INVITER_BONUS_KOPEKS, commission_amount) + inviter_bonus = settings.REFERRAL_INVITER_BONUS_KOPEKS + commission_amount if inviter_bonus > 0: balance_ok = await add_user_balance( @@ -332,10 +332,22 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko ) if bot: + bonus_parts = [] + if settings.REFERRAL_INVITER_BONUS_KOPEKS > 0: + bonus_parts.append( + f'фикс. бонус {settings.format_price(settings.REFERRAL_INVITER_BONUS_KOPEKS)}' + ) + if commission_amount > 0: + bonus_parts.append( + f'комиссия {commission_percent}% = {settings.format_price(commission_amount)}' + ) + bonus_breakdown = ' + '.join(bonus_parts) inviter_bonus_notification = ( f'💰 Реферальная награда!\n\n' - f'Ваш реферал {user.full_name} сделал первое пополнение!\n\n' - f'🎁 Вы получили награду: {settings.format_price(inviter_bonus)}\n\n' + f'Ваш реферал {user.full_name} сделал первое пополнение ' + f'на {settings.format_price(topup_amount_kopeks)}!\n\n' + f'🎁 Ваша награда: {settings.format_price(inviter_bonus)}' + f' ({bonus_breakdown})\n\n' f'📈 Теперь с каждого его пополнения вы будете получать {commission_percent}% комиссии.' ) await send_referral_notification( diff --git a/app/services/tribute_service.py b/app/services/tribute_service.py index 003b7ee2..6e5b8a24 100644 --- a/app/services/tribute_service.py +++ b/app/services/tribute_service.py @@ -166,7 +166,7 @@ class TributeService: except Exception as e: logger.error('Ошибка обработки реферального пополнения Tribute', error=e) - if was_first_topup and not user.has_made_first_topup: + if was_first_topup and not user.has_made_first_topup and not user.referred_by_id: user.has_made_first_topup = True await session.commit() diff --git a/tests/services/test_referral_service.py b/tests/services/test_referral_service.py index abdb9029..216af107 100644 --- a/tests/services/test_referral_service.py +++ b/tests/services/test_referral_service.py @@ -36,6 +36,7 @@ async def test_commission_accrues_before_minimum_first_topup(monkeypatch): monkeypatch.setattr(referral_service, 'add_user_balance', add_user_balance_mock) create_referral_earning_mock = AsyncMock() monkeypatch.setattr(referral_service, 'create_referral_earning', create_referral_earning_mock) + monkeypatch.setattr(referral_service, 'get_user_campaign_id', AsyncMock(return_value=None)) monkeypatch.setattr(referral_service.settings, 'REFERRAL_MINIMUM_TOPUP_KOPEKS', 20000) monkeypatch.setattr(referral_service.settings, 'REFERRAL_FIRST_TOPUP_BONUS_KOPEKS', 5000) @@ -61,5 +62,58 @@ async def test_commission_accrues_before_minimum_first_topup(monkeypatch): assert earning_call.kwargs['amount_kopeks'] == 3750 assert earning_call.kwargs['reason'] == 'referral_commission_topup' - db.commit.assert_not_awaited() - db.execute.assert_not_awaited() + +async def test_first_topup_inviter_gets_fixed_plus_commission(monkeypatch): + """Inviter bonus should be fixed bonus + commission, not max(fixed, commission).""" + user = SimpleNamespace( + id=1, + telegram_id=101, + full_name='Test User', + referred_by_id=2, + has_made_first_topup=False, + ) + referrer = SimpleNamespace( + id=2, + telegram_id=202, + full_name='Referrer', + email=None, + ) + + db = SimpleNamespace( + commit=AsyncMock(), + execute=AsyncMock(), + ) + + get_user_mock = AsyncMock(side_effect=[user, referrer]) + monkeypatch.setattr(referral_service, 'get_user_by_id', get_user_mock) + add_user_balance_mock = AsyncMock(return_value=True) + monkeypatch.setattr(referral_service, 'add_user_balance', add_user_balance_mock) + create_referral_earning_mock = AsyncMock() + monkeypatch.setattr(referral_service, 'create_referral_earning', create_referral_earning_mock) + monkeypatch.setattr(referral_service, 'get_commission_payment_count', AsyncMock(return_value=0)) + monkeypatch.setattr(referral_service, 'get_user_campaign_id', AsyncMock(return_value=None)) + monkeypatch.setattr(referral_service, 'get_effective_referral_commission_percent', lambda u: 15) + + monkeypatch.setattr(referral_service.settings, 'REFERRAL_MINIMUM_TOPUP_KOPEKS', 10000) + monkeypatch.setattr(referral_service.settings, 'REFERRAL_FIRST_TOPUP_BONUS_KOPEKS', 5000) + monkeypatch.setattr(referral_service.settings, 'REFERRAL_INVITER_BONUS_KOPEKS', 5000) # 50 rub + monkeypatch.setattr(referral_service.settings, 'REFERRAL_COMMISSION_PERCENT', 15) + + topup_amount = 50000 # 500 rub + + result = await referral_service.process_referral_topup(db, user.id, topup_amount) + + assert result is True + assert user.has_made_first_topup is True + + # add_user_balance called twice: first for referral's own bonus, then for inviter bonus + assert add_user_balance_mock.await_count == 2 + + # Second call is the inviter bonus: fixed 5000 + commission 15% of 50000 = 7500 → total 12500 + inviter_call = add_user_balance_mock.await_args_list[1] + expected_commission = int(50000 * 15 / 100) # 7500 + expected_inviter_bonus = 5000 + expected_commission # 12500 + assert inviter_call.args[2] == expected_inviter_bonus + + # With old max() logic, this would have been max(5000, 7500) = 7500 — wrong! + assert expected_inviter_bonus == 12500