fix: реферальный бонус инвайтера — сумма вместо максимума, защита флага первого пополнения

- referral_service: inviter_bonus = fixed + commission вместо max(fixed, commission)
- 13 платёжных провайдеров: has_made_first_topup ставится только для нереферальных юзеров
- riopay: критический фикс — флаг ставился до вызова referral_service
- Обновлены уведомления с разбивкой бонуса
- Исправлен и дополнен тест referral_service
This commit is contained in:
Fringg
2026-03-16 03:57:50 +03:00
parent 3d68db0a51
commit e1bcb1ba91
15 changed files with 86 additions and 20 deletions
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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()
+17 -5
View File
@@ -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'💰 <b>Реферальная награда!</b>\n\n'
f'Ваш реферал <b>{user.full_name}</b> сделал первое пополнение!\n\n'
f'🎁 Вы получили награду: {settings.format_price(inviter_bonus)}\n\n'
f'Ваш реферал <b>{user.full_name}</b> сделал первое пополнение '
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(
+1 -1
View File
@@ -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()
+56 -2
View File
@@ -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