fix: 3 bugs — notification type, referral with channel sub, BOT_USERNAME

1. Admin notification showed "renewal" instead of "first purchase" for new
   users because has_had_paid_subscription was set before notification.
   All 21 call sites now pass explicit purchase_type.

2. Partner referral not counted when mandatory channel subscription enabled.
   required_sub_channel_check saved campaign_id but not referrer_id from
   campaign.partner_user_id. Also removed duplicate DB query.

3. BOT_USERNAME auto-detection moved before web server start to close
   race window on /cabinet/branding/telegram-widget endpoint.
This commit is contained in:
Fringg
2026-03-10 21:18:58 +03:00
parent 541f64d5bc
commit 3c96c2affd
13 changed files with 40 additions and 18 deletions
+2 -2
View File
@@ -1904,7 +1904,7 @@ async def submit_purchase(
period_days=selection.period.days,
was_trial_conversion=result.get('was_trial_conversion', False),
amount_kopeks=pricing.final_total,
purchase_type='renewal' if not is_new_subscription else None,
purchase_type='renewal' if not is_new_subscription else 'first_purchase',
)
finally:
await bot.session.close()
@@ -2357,7 +2357,7 @@ async def purchase_tariff(
period_days=period_days,
was_trial_conversion=False,
amount_kopeks=price_kopeks,
purchase_type='renewal' if not was_new_subscription else None,
purchase_type='renewal' if not was_new_subscription else 'first_purchase',
)
finally:
await bot.session.close()
+2
View File
@@ -649,6 +649,7 @@ async def handle_simple_subscription_pay_with_balance(
subscription_params['period_days'],
False, # was_trial_conversion
amount_kopeks=price_kopeks,
purchase_type='renewal' if existing_subscription else 'first_purchase',
)
except Exception as e:
logger.error('Ошибка отправки уведомления админам о покупке', error=e)
@@ -2368,6 +2369,7 @@ async def confirm_simple_subscription_purchase(
subscription_params['period_days'],
False, # was_trial_conversion
amount_kopeks=price_kopeks,
purchase_type='renewal' if existing_subscription else 'first_purchase',
)
except Exception as e:
logger.error('Ошибка отправки уведомления админам о покупке', error=e)
+7 -9
View File
@@ -2118,14 +2118,6 @@ async def required_sub_channel_check(
# Очищаем Redis после успешной проверки подписки
await delete_pending_payload_from_redis(query.from_user.id)
# Всегда обновляем referral_code если есть новый payload
# (исправление бага с устаревшими данными в state)
campaign = await get_campaign_by_start_parameter(
db,
pending_start_payload,
only_active=True,
)
# Обрабатываем payload только если ещё не обработан
# (проверяем по наличию referral_code или campaign_id в state)
if not state_data.get('referral_code') and not state_data.get('campaign_id'):
@@ -2137,7 +2129,13 @@ async def required_sub_channel_check(
if campaign:
state_data['campaign_id'] = campaign.id
logger.info('📣 CHANNEL CHECK: Кампания восстановлена из payload', campaign_id=campaign.id)
if campaign.partner_user_id:
state_data['referrer_id'] = campaign.partner_user_id
logger.info(
'📣 CHANNEL CHECK: Кампания восстановлена из payload',
campaign_id=campaign.id,
partner_user_id=campaign.partner_user_id,
)
else:
state_data['referral_code'] = pending_start_payload
logger.info(
+3 -1
View File
@@ -27,6 +27,7 @@ async def send_purchase_notification(
transaction_id: int,
period_days: int,
was_trial_conversion: bool = False,
purchase_type: str | None = None,
):
try:
from app.database.crud.transaction import get_transaction_by_id
@@ -35,7 +36,8 @@ async def send_purchase_notification(
if transaction:
notification_service = AdminNotificationService(callback.bot)
await notification_service.send_subscription_purchase_notification(
db, db_user, subscription, transaction, period_days, was_trial_conversion
db, db_user, subscription, transaction, period_days, was_trial_conversion,
purchase_type=purchase_type,
)
except Exception as e:
logger.error('Ошибка отправки уведомления о покупке', error=e)
+2 -1
View File
@@ -2821,7 +2821,8 @@ async def confirm_purchase(callback: types.CallbackQuery, state: FSMContext, db_
try:
notification_service = AdminNotificationService(callback.bot)
await notification_service.send_subscription_purchase_notification(
db, db_user, subscription, transaction, period_days, was_trial_conversion
db, db_user, subscription, transaction, period_days, was_trial_conversion,
purchase_type='renewal' if existing_subscription else 'first_purchase',
)
except Exception as e:
logger.error('Ошибка отправки уведомления о покупке', error=e)
@@ -909,6 +909,7 @@ async def handle_custom_confirm(
custom_days,
was_trial_conversion=False,
amount_kopeks=total_price,
purchase_type='renewal' if existing_subscription else 'first_purchase',
)
except Exception as e:
logger.error('Ошибка отправки уведомления админу', error=e)
@@ -1221,6 +1222,7 @@ async def confirm_tariff_purchase(
period,
was_trial_conversion=False,
amount_kopeks=final_price,
purchase_type='renewal' if existing_subscription else 'first_purchase',
)
except Exception as e:
logger.error('Ошибка отправки уведомления админу', error=e)
@@ -1404,6 +1406,7 @@ async def confirm_daily_tariff_purchase(
1, # 1 день
was_trial_conversion=False,
amount_kopeks=daily_price,
purchase_type='renewal' if existing_subscription else 'first_purchase',
)
except Exception as e:
logger.error('Ошибка отправки уведомления админу', error=e)
@@ -1765,6 +1768,7 @@ async def confirm_tariff_extend(
period,
was_trial_conversion=False,
amount_kopeks=final_price,
purchase_type='renewal',
)
except Exception as e:
logger.error('Ошибка отправки уведомления админу', error=e)
+5 -2
View File
@@ -401,7 +401,7 @@ class AdminNotificationService:
period_days: int,
was_trial_conversion: bool = False,
amount_kopeks: int | None = None,
purchase_type: str | None = None, # 'tariff_switch', 'renewal', None (auto)
purchase_type: str | None = None, # 'first_purchase', 'renewal', 'tariff_switch', None (auto-detect)
) -> bool:
try:
total_amount = (
@@ -436,7 +436,10 @@ class AdminNotificationService:
elif was_trial_conversion:
event_title = '🔄 КОНВЕРСИЯ ИЗ ТРИАЛА'
user_status = 'Конверсия'
elif purchase_type == 'renewal' or user.has_had_paid_subscription:
elif purchase_type == 'first_purchase':
event_title = '💎 ПОКУПКА ПОДПИСКИ'
user_status = 'Первая покупка'
elif purchase_type == 'renewal' or (purchase_type is None and user.has_had_paid_subscription):
event_title = '💎 ПРОДЛЕНИЕ ПОДПИСКИ'
user_status = 'Продление'
else:
+1
View File
@@ -345,6 +345,7 @@ class TelegramStarsMixin:
transaction,
period_display,
was_trial_conversion=False,
purchase_type='renewal' if user.has_had_paid_subscription else 'first_purchase',
)
except Exception as admin_error: # pragma: no cover - диагностический лог
logger.error(
+1
View File
@@ -976,6 +976,7 @@ class YooKassaPaymentMixin:
transaction,
subscription_period,
was_trial_conversion=False,
purchase_type='renewal' if (full_user or user).has_had_paid_subscription else 'first_purchase',
)
except Exception as admin_error:
logger.error(
@@ -825,7 +825,8 @@ async def _auto_purchase_tariff(
await with_admin_notification_service(
lambda svc: svc.send_subscription_purchase_notification(
db, user, subscription, transaction, period_days, was_trial_conversion
db, user, subscription, transaction, period_days, was_trial_conversion,
purchase_type='renewal',
)
)
except Exception as error:
@@ -1100,7 +1101,8 @@ async def _auto_purchase_daily_tariff(
await with_admin_notification_service(
lambda svc: svc.send_subscription_purchase_notification(
db, user, subscription, transaction, 1, was_trial_conversion
db, user, subscription, transaction, 1, was_trial_conversion,
purchase_type='renewal',
)
)
except Exception as error:
@@ -2398,6 +2400,7 @@ async def auto_purchase_saved_cart_after_topup(
transaction,
selection.period.days,
was_trial_conversion,
purchase_type='renewal',
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
+2
View File
@@ -5670,6 +5670,7 @@ async def subscription_purchase_endpoint(
pass
if subscription and transaction and period_days:
_purchase_type = 'renewal' if context.subscription else 'first_purchase'
await with_admin_notification_service(
lambda service: service.send_subscription_purchase_notification(
db,
@@ -5678,6 +5679,7 @@ async def subscription_purchase_endpoint(
transaction,
period_days,
was_trial_conversion=was_trial_conversion,
purchase_type=_purchase_type,
)
)
+5 -1
View File
@@ -295,6 +295,11 @@ async def main():
bot, dp = await setup_bot()
stage.log('Кеш и FSM подготовлены')
bot_user = await bot.get_me()
if bot_user.username and not settings.BOT_USERNAME:
settings.BOT_USERNAME = bot_user.username
logger.info('BOT_USERNAME auto-detected', bot_username=bot_user.username)
monitoring_service.bot = bot
maintenance_service.set_bot(bot)
broadcast_service.set_bot(bot)
@@ -516,7 +521,6 @@ async def main():
success_message='Токен внешней админки готов',
) as stage:
try:
bot_user = await bot.get_me()
token = await ensure_external_admin_token(
bot_user.username,
bot_user.id,
@@ -273,6 +273,7 @@ async def test_process_stars_payment_simple_subscription_success(
transaction: Any,
period_days: int,
was_trial_conversion: bool,
**kwargs: Any,
) -> None:
admin_calls.append(
{