fix: correct referral withdrawal balance formula and commission transaction type

The available_referral formula incorrectly treated all post-earning spending
as spent from referral balance, making withdrawable balance stay at 0 even
as earnings increased. Changed to min(wallet_balance, earned - withdrawn - pending).

- Fix available_referral in withdrawal service and referral info endpoint
- Use TransactionType.REFERRAL_REWARD for all commission/bonus balance additions
- Gate create_referral_earning behind add_user_balance success check
- Move notifications inside balance_ok guards to prevent false confirmations
This commit is contained in:
Fringg
2026-03-02 01:35:24 +03:00
parent ed3ae14d0c
commit 83c6db4834
3 changed files with 166 additions and 118 deletions

View File

@@ -76,7 +76,9 @@ async def get_referral_info(
pending_result = await db.execute(pending_query)
pending = pending_result.scalar() or 0
available_balance = max(0, total_earnings - withdrawn - pending)
# Доступный баланс: мин(кошелёк, заработано - выведено - в ожидании)
referral_entitlement = max(0, total_earnings - withdrawn - pending)
available_balance = min(user.balance_kopeks, referral_entitlement)
# Build referral link
bot_username = settings.get_bot_username() or 'bot'

View File

@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.crud.referral import create_referral_earning, get_user_campaign_id
from app.database.crud.user import add_user_balance, get_user_by_id
from app.database.models import ReferralEarning, User
from app.database.models import ReferralEarning, TransactionType, User
from app.services.notification_delivery_service import (
notification_delivery_service,
)
@@ -168,45 +168,53 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
)
if commission_amount > 0:
await add_user_balance(
balance_ok = await add_user_balance(
db,
referrer,
commission_amount,
f'Комиссия {commission_percent}% с пополнения {user.full_name}',
transaction_type=TransactionType.REFERRAL_REWARD,
bot=bot,
)
await create_referral_earning(
db=db,
user_id=referrer.id,
referral_id=user.id,
amount_kopeks=commission_amount,
reason='referral_commission_topup',
campaign_id=campaign_id,
)
logger.info(
'💰 Комиссия с пополнения: получил ₽ (до первого бонуса)',
telegram_id=referrer.telegram_id,
commission_amount=commission_amount / 100,
)
if bot:
commission_notification = (
f'💰 <b>Реферальная комиссия!</b>\n\n'
f'Ваш реферал <b>{user.full_name}</b> пополнил баланс на '
f'{settings.format_price(topup_amount_kopeks)}\n\n'
f'🎁 Ваша комиссия ({commission_percent}%): '
f'{settings.format_price(commission_amount)}\n\n'
f'💎 Средства зачислены на ваш баланс.'
if balance_ok:
await create_referral_earning(
db=db,
user_id=referrer.id,
referral_id=user.id,
amount_kopeks=commission_amount,
reason='referral_commission_topup',
campaign_id=campaign_id,
)
await send_referral_notification(
bot,
referrer.telegram_id,
commission_notification,
user=referrer,
bonus_kopeks=commission_amount,
referral_name=user.full_name,
logger.info(
'💰 Комиссия с пополнения: получил ₽ (до первого бонуса)',
telegram_id=referrer.telegram_id,
commission_amount=commission_amount / 100,
)
if bot:
commission_notification = (
f'💰 <b>Реферальная комиссия!</b>\n\n'
f'Ваш реферал <b>{user.full_name}</b> пополнил баланс на '
f'{settings.format_price(topup_amount_kopeks)}\n\n'
f'🎁 Ваша комиссия ({commission_percent}%): '
f'{settings.format_price(commission_amount)}\n\n'
f'💎 Средства зачислены на ваш баланс.'
)
await send_referral_notification(
bot,
referrer.telegram_id,
commission_notification,
user=referrer,
bonus_kopeks=commission_amount,
referral_name=user.full_name,
)
else:
logger.error(
'Не удалось начислить комиссию на баланс, ReferralEarning не создан',
referrer_id=referrer.id,
commission_amount=commission_amount,
)
return True
@@ -228,31 +236,39 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
logger.error('Ошибка удаления записи ожидания', error=e)
if settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS > 0:
await add_user_balance(
bonus_ok = await add_user_balance(
db,
user,
settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS,
'Бонус за первое пополнение по реферальной программе',
transaction_type=TransactionType.REFERRAL_REWARD,
bot=bot,
)
logger.info(
'💰 Реферал получил бонус ₽',
user_id=user.id,
REFERRAL_FIRST_TOPUP_BONUS_KOPEKS=settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS / 100,
)
if bot:
bonus_notification = (
f'🎉 <b>Бонус получен!</b>\n\n'
f'За первое пополнение вы получили бонус '
f'{settings.format_price(settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS)}!\n\n'
f'💎 Средства зачислены на ваш баланс.'
if bonus_ok:
logger.info(
'💰 Реферал получил бонус ₽',
user_id=user.id,
REFERRAL_FIRST_TOPUP_BONUS_KOPEKS=settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS / 100,
)
await send_referral_notification(
bot,
user.telegram_id,
bonus_notification,
user=user,
if bot:
bonus_notification = (
f'🎉 <b>Бонус получен!</b>\n\n'
f'За первое пополнение вы получили бонус '
f'{settings.format_price(settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS)}!\n\n'
f'💎 Средства зачислены на ваш баланс.'
)
await send_referral_notification(
bot,
user.telegram_id,
bonus_notification,
user=user,
bonus_kopeks=settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS,
)
else:
logger.error(
'Не удалось начислить бонус за первое пополнение',
user_id=user.id,
bonus_kopeks=settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS,
)
@@ -260,78 +276,99 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
inviter_bonus = max(settings.REFERRAL_INVITER_BONUS_KOPEKS, commission_amount)
if inviter_bonus > 0:
await add_user_balance(
db, referrer, inviter_bonus, f'Бонус за первое пополнение реферала {user.full_name}', bot=bot
balance_ok = await add_user_balance(
db,
referrer,
inviter_bonus,
f'Бонус за первое пополнение реферала {user.full_name}',
transaction_type=TransactionType.REFERRAL_REWARD,
bot=bot,
)
await create_referral_earning(
db=db,
user_id=referrer.id,
referral_id=user.id,
amount_kopeks=inviter_bonus,
reason='referral_first_topup',
campaign_id=campaign_id,
)
referrer_id = referrer.telegram_id or referrer.email or f'user#{referrer.id}'
logger.info('💰 Реферер получил бонус ₽', referrer_id=referrer_id, inviter_bonus=inviter_bonus / 100)
if bot:
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'📈 Теперь с каждого его пополнения вы будете получать {commission_percent}% комиссии.'
if balance_ok:
await create_referral_earning(
db=db,
user_id=referrer.id,
referral_id=user.id,
amount_kopeks=inviter_bonus,
reason='referral_first_topup',
campaign_id=campaign_id,
)
await send_referral_notification(
bot,
referrer.telegram_id,
inviter_bonus_notification,
user=referrer,
bonus_kopeks=inviter_bonus,
referral_name=user.full_name,
referrer_id = referrer.telegram_id or referrer.email or f'user#{referrer.id}'
logger.info('💰 Реферер получил бонус ₽', referrer_id=referrer_id, inviter_bonus=inviter_bonus / 100)
if bot:
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'📈 Теперь с каждого его пополнения вы будете получать {commission_percent}% комиссии.'
)
await send_referral_notification(
bot,
referrer.telegram_id,
inviter_bonus_notification,
user=referrer,
bonus_kopeks=inviter_bonus,
referral_name=user.full_name,
)
else:
logger.error(
'Не удалось начислить бонус на баланс, ReferralEarning не создан',
referrer_id=referrer.id,
inviter_bonus=inviter_bonus,
)
elif commission_amount > 0:
await add_user_balance(
balance_ok = await add_user_balance(
db,
referrer,
commission_amount,
f'Комиссия {commission_percent}% с пополнения {user.full_name}',
transaction_type=TransactionType.REFERRAL_REWARD,
bot=bot,
)
await create_referral_earning(
db=db,
user_id=referrer.id,
referral_id=user.id,
amount_kopeks=commission_amount,
reason='referral_commission_topup',
campaign_id=campaign_id,
)
referrer_id = referrer.telegram_id or referrer.email or f'user#{referrer.id}'
logger.info(
'💰 Комиссия с пополнения: получил ₽',
referrer_id=referrer_id,
commission_amount=commission_amount / 100,
)
if bot:
commission_notification = (
f'💰 <b>Реферальная комиссия!</b>\n\n'
f'Ваш реферал <b>{user.full_name}</b> пополнил баланс на '
f'{settings.format_price(topup_amount_kopeks)}\n\n'
f'🎁 Ваша комиссия ({commission_percent}%): '
f'{settings.format_price(commission_amount)}\n\n'
f'💎 Средства зачислены на ваш баланс.'
if balance_ok:
await create_referral_earning(
db=db,
user_id=referrer.id,
referral_id=user.id,
amount_kopeks=commission_amount,
reason='referral_commission_topup',
campaign_id=campaign_id,
)
await send_referral_notification(
bot,
referrer.telegram_id,
commission_notification,
user=referrer,
bonus_kopeks=commission_amount,
referral_name=user.full_name,
referrer_id = referrer.telegram_id or referrer.email or f'user#{referrer.id}'
logger.info(
'💰 Комиссия с пополнения: получил ₽',
referrer_id=referrer_id,
commission_amount=commission_amount / 100,
)
if bot:
commission_notification = (
f'💰 <b>Реферальная комиссия!</b>\n\n'
f'Ваш реферал <b>{user.full_name}</b> пополнил баланс на '
f'{settings.format_price(topup_amount_kopeks)}\n\n'
f'🎁 Ваша комиссия ({commission_percent}%): '
f'{settings.format_price(commission_amount)}\n\n'
f'💎 Средства зачислены на ваш баланс.'
)
await send_referral_notification(
bot,
referrer.telegram_id,
commission_notification,
user=referrer,
bonus_kopeks=commission_amount,
referral_name=user.full_name,
)
else:
logger.error(
'Не удалось начислить комиссию на баланс, ReferralEarning не создан',
referrer_id=referrer.id,
commission_amount=commission_amount,
)
return True

View File

@@ -118,6 +118,10 @@ class ReferralWithdrawalService:
async def get_referral_balance_stats(self, db: AsyncSession, user_id: int) -> dict:
"""
Получает полную статистику реферального баланса.
Доступный реферальный баланс = min(баланс кошелька, заработано - выведено - в ожидании).
Партнёр не может вывести больше, чем реально лежит в кошельке (User.balance_kopeks),
и не больше, чем заработал минус уже выведенные/замороженные средства.
"""
total_earned = await self.get_total_referral_earnings(db, user_id)
own_deposits = await self.get_user_own_deposits(db, user_id)
@@ -126,27 +130,32 @@ class ReferralWithdrawalService:
withdrawn = await self.get_withdrawn_amount(db, user_id)
pending = await self.get_pending_withdrawal_amount(db, user_id)
# Сколько реф. баланса потрачено = мин(траты ПОСЛЕ первого начисления, реф_заработок)
# Логика: только траты после получения реф. дохода могут быть из реф. баланса
# Текущий баланс кошелька — реальный ограничитель вывода
user = await db.get(User, user_id)
user_balance = user.balance_kopeks if user else 0
# referral_spent — для аналитики/отображения, больше НЕ влияет на available_referral
referral_spent = min(spending_after_earning, total_earned)
# Доступный реферальный баланс
available_referral = max(0, total_earned - referral_spent - withdrawn - pending)
# Реферальное право: сколько заработано минус выведено/заморожено
referral_entitlement = max(0, total_earned - withdrawn - pending)
# Доступный реферальный баланс: мин(кошелёк, реферальное право)
# Нельзя вывести больше, чем лежит в кошельке
available_referral = min(user_balance, referral_entitlement)
# Если разрешено выводить и свой баланс
if not settings.REFERRAL_WITHDRAWAL_ONLY_REFERRAL_BALANCE:
# Свой остаток = пополнения - (траты - реф_потрачено)
own_remaining = max(0, own_deposits - max(0, spending - referral_spent))
available_total = available_referral + own_remaining
# Весь кошелёк доступен к выводу (уже ограничен user_balance)
available_total = user_balance
else:
own_remaining = 0
available_total = available_referral
return {
'total_earned': total_earned, # Всего заработано с рефералов
'own_deposits': own_deposits, # Собственные пополнения
'spending': spending, # Потрачено на подписки и пр.
'referral_spent': referral_spent, # Сколько реф. баланса потрачено
'referral_spent': referral_spent, # Сколько реф. баланса потрачено (аналитика)
'withdrawn': withdrawn, # Уже выведено
'pending': pending, # На рассмотрении
'available_referral': available_referral, # Доступно реф. баланса