Merge pull request #577 from Fr1ngg/bedolaga/add-admin-notifications-for-promo-groups

Enhance admin notifications with promo group context
This commit is contained in:
Egor
2025-09-30 01:57:14 +03:00
committed by GitHub
5 changed files with 316 additions and 12 deletions
+25
View File
@@ -20,6 +20,7 @@ from app.keyboards.admin import (
get_admin_users_filters_keyboard, get_user_promo_group_keyboard
)
from app.localization.texts import get_texts
from app.services.admin_notification_service import AdminNotificationService
from app.services.user_service import UserService
from app.database.crud.promo_group import get_promo_groups_with_counts
from app.utils.decorators import admin_required, error_handler
@@ -999,6 +1000,13 @@ async def set_user_promo_group(
await callback.answer(texts.ADMIN_USER_PROMO_GROUP_ALREADY, show_alert=True)
return
try:
await db.refresh(user, ["promo_group"])
except Exception:
pass
previous_group = getattr(user, "promo_group", None)
user_service = UserService()
success, updated_user, new_group = await user_service.update_user_promo_group(
db,
@@ -1010,6 +1018,23 @@ async def set_user_promo_group(
await callback.answer(texts.ADMIN_USER_PROMO_GROUP_ERROR, show_alert=True)
return
try:
notification_service = AdminNotificationService(callback.bot)
await notification_service.send_promo_group_change_notification(
db,
updated_user,
previous_group,
new_group,
changed_by=db_user,
change_source="manual",
)
except Exception as notification_error:
logger.error(
"Ошибка отправки админ-уведомления о смене промогруппы пользователя %s: %s",
user_id,
notification_error,
)
promo_groups = await get_promo_groups_with_counts(db)
await _render_user_promo_group(callback.message, db_user.language, updated_user, promo_groups)
+18
View File
@@ -8,6 +8,7 @@ from app.states import PromoCodeStates
from app.database.models import User
from app.keyboards.inline import get_back_keyboard
from app.localization.texts import get_texts
from app.services.admin_notification_service import AdminNotificationService
from app.services.promocode_service import PromoCodeService
from app.utils.decorators import error_handler
@@ -61,6 +62,23 @@ async def process_promocode(
reply_markup=get_back_keyboard(db_user.language)
)
logger.info(f"✅ Пользователь {db_user.telegram_id} активировал промокод {code}")
promocode_obj = result.get("promocode")
if promocode_obj:
try:
notification_service = AdminNotificationService(message.bot)
await notification_service.send_promocode_activation_notification(
db,
db_user,
promocode_obj,
result.get("description", ""),
)
except Exception as notification_error:
logger.error(
"Ошибка отправки админ-уведомления об активации промокода %s: %s",
code,
notification_error,
)
else:
error_messages = {
"not_found": texts.PROMOCODE_INVALID,
+17
View File
@@ -25,6 +25,7 @@ from app.localization.loader import DEFAULT_LANGUAGE
from app.localization.texts import get_texts, get_rules
from app.services.referral_service import process_referral_registration
from app.services.campaign_service import AdvertisingCampaignService
from app.services.admin_notification_service import AdminNotificationService
from app.services.subscription_service import SubscriptionService
from app.utils.user_utils import generate_unique_referral_code
from app.database.crud.user_message import get_random_active_message
@@ -165,6 +166,22 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
await state.update_data(referral_code=referral_code)
user = db_user if db_user else await get_user_by_telegram_id(db, message.from_user.id)
if campaign:
try:
notification_service = AdminNotificationService(message.bot)
await notification_service.send_campaign_click_notification(
db,
campaign,
message.from_user,
user,
)
except Exception as notification_error:
logger.error(
"Ошибка отправки уведомления о переходе по кампании %s: %s",
campaign.id if campaign else 'unknown',
notification_error,
)
if user and user.status != UserStatus.DELETED.value:
logger.info(f"✅ Активный пользователь найден: {user.telegram_id}")
+254 -11
View File
@@ -7,7 +7,16 @@ from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import User, Subscription, Transaction, TransactionType
from app.database.models import (
AdvertisingCampaign,
PromoCode,
PromoCodeType,
PromoGroup,
Subscription,
Transaction,
TransactionType,
User,
)
from app.database.crud.user import get_user_by_id
logger = logging.getLogger(__name__)
@@ -48,11 +57,12 @@ class AdminNotificationService:
) -> bool:
if not self._is_enabled():
return False
try:
user_status = "🆕 Новый" if not user.has_had_paid_subscription else "🔄 Существующий"
referrer_info = await self._get_referrer_info(db, user.referred_by_id)
promo_group_info = await self._format_user_promo_group_info(db, user)
message = f"""🎯 <b>АКТИВАЦИЯ ТРИАЛА</b>
👤 <b>Пользователь:</b> {user.full_name}
@@ -60,6 +70,8 @@ class AdminNotificationService:
📱 <b>Username:</b> @{user.username or 'отсутствует'}
👥 <b>Статус:</b> {user_status}
{promo_group_info}
⏰ <b>Параметры триала:</b>
📅 Период: {settings.TRIAL_DURATION_DAYS} дней
📊 Трафик: {settings.TRIAL_TRAFFIC_LIMIT_GB} ГБ
@@ -98,11 +110,12 @@ class AdminNotificationService:
user_status = "🔄 Продление/Обновление"
else:
user_status = "🆕 Первая покупка"
servers_info = await self._get_servers_info(subscription.connected_squads)
payment_method = self._get_payment_method_display(transaction.payment_method)
referrer_info = await self._get_referrer_info(db, user.referred_by_id)
promo_group_info = await self._format_user_promo_group_info(db, user)
message = f"""💎 <b>{event_type}</b>
👤 <b>Пользователь:</b> {user.full_name}
@@ -110,6 +123,8 @@ class AdminNotificationService:
📱 <b>Username:</b> @{user.username or 'отсутствует'}
👥 <b>Статус:</b> {user_status}
{promo_group_info}
💰 <b>Платеж:</b>
💵 Сумма: {settings.format_price(transaction.amount_kopeks)}
💳 Способ: {payment_method}
@@ -230,6 +245,7 @@ class AdminNotificationService:
payment_method = self._get_payment_method_display(transaction.payment_method)
balance_change = user.balance_kopeks - old_balance
referrer_info = await self._get_referrer_info(db, user.referred_by_id)
promo_group_info = await self._format_user_promo_group_info(db, user)
subscription_result = await db.execute(
select(Subscription).where(Subscription.user_id == user.id)
)
@@ -243,6 +259,8 @@ class AdminNotificationService:
📱 <b>Username:</b> @{user.username or 'отсутствует'}
💳 <b>Статус:</b> {topup_status}
{promo_group_info}
💰 <b>Детали пополнения:</b>
💵 Сумма: {settings.format_price(transaction.amount_kopeks)}
💳 Способ: {payment_method}
@@ -279,13 +297,16 @@ class AdminNotificationService:
try:
payment_method = self._get_payment_method_display(transaction.payment_method)
servers_info = await self._get_servers_info(subscription.connected_squads)
promo_group_info = await self._format_user_promo_group_info(db, user)
message = f"""⏰ <b>ПРОДЛЕНИЕ ПОДПИСКИ</b>
👤 <b>Пользователь:</b> {user.full_name}
🆔 <b>Telegram ID:</b> <code>{user.telegram_id}</code>
📱 <b>Username:</b> @{user.username or 'отсутствует'}
{promo_group_info}
💰 <b>Платеж:</b>
💵 Сумма: {settings.format_price(transaction.amount_kopeks)}
💳 Способ: {payment_method}
@@ -748,21 +769,24 @@ class AdminNotificationService:
try:
referrer_info = await self._get_referrer_info(db, user.referred_by_id)
promo_group_info = await self._format_user_promo_group_info(db, user)
update_types = {
"traffic": ("📊 ИЗМЕНЕНИЕ ТРАФИКА", "трафик"),
"devices": ("📱 ИЗМЕНЕНИЕ УСТРОЙСТВ", "количество устройств"),
"devices": ("📱 ИЗМЕНЕНИЕ УСТРОЙСТВ", "количество устройств"),
"servers": ("🌐 ИЗМЕНЕНИЕ СЕРВЕРОВ", "серверы")
}
title, param_name = update_types.get(update_type, ("⚙️ ИЗМЕНЕНИЕ ПОДПИСКИ", "параметры"))
message = f"""{title}
👤 <b>Пользователь:</b> {user.full_name}
🆔 <b>Telegram ID:</b> <code>{user.telegram_id}</code>
📱 <b>Username:</b> @{user.username or 'отсутствует'}
{promo_group_info}
🔧 <b>Изменение:</b>
📋 Параметр: {param_name}"""
@@ -790,13 +814,173 @@ class AdminNotificationService:
🔗 <b>Рефер:</b> {referrer_info}
⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
return await self._send_message(message)
except Exception as e:
logger.error(f"Ошибка отправки уведомления об изменении подписки: {e}")
return False
async def send_promocode_activation_notification(
self,
db: AsyncSession,
user: User,
promocode: PromoCode,
result_description: str
) -> bool:
if not self._is_enabled():
return False
try:
referrer_info = await self._get_referrer_info(db, user.referred_by_id)
promo_group_info = await self._format_user_promo_group_info(db, user)
promocode_type_display = {
PromoCodeType.BALANCE.value: "💰 Баланс",
PromoCodeType.SUBSCRIPTION_DAYS.value: "⏰ Продление подписки",
PromoCodeType.TRIAL_SUBSCRIPTION.value: "🎯 Триал",
}.get(promocode.type, promocode.type)
uses_text = f"{promocode.current_uses}/{promocode.max_uses}" if promocode.max_uses else f"{promocode.current_uses}"
valid_until = (
promocode.valid_until.strftime('%d.%m.%Y %H:%M')
if promocode.valid_until
else "Бессрочно"
)
bonus_parts: List[str] = []
if promocode.balance_bonus_kopeks > 0:
bonus_parts.append(f"💰 Баланс: {settings.format_price(promocode.balance_bonus_kopeks)}")
if promocode.subscription_days > 0:
bonus_parts.append(f"⏱ Дней подписки: {promocode.subscription_days}")
if not bonus_parts:
bonus_parts.append("ℹ️ Дополнительных бонусов нет")
message = f"""🎫 <b>АКТИВАЦИЯ ПРОМОКОДА</b>
👤 <b>Пользователь:</b> {user.full_name}
🆔 <b>Telegram ID:</b> <code>{user.telegram_id}</code>
📱 <b>Username:</b> @{user.username or 'отсутствует'}
{promo_group_info}
🔗 <b>Реферер:</b> {referrer_info}
🎟 <b>Промокод:</b> <code>{promocode.code}</code>
🏷 <b>Тип:</b> {promocode_type_display}
📊 <b>Использования:</b> {uses_text}
⏳ <b>Действует до:</b> {valid_until}
🎁 <b>Бонусы:</b> {'; '.join(bonus_parts)}
📝 <b>Результат:</b> {result_description or 'Успешная активация'}
⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
return await self._send_message(message)
except Exception as e:
logger.error(f"Ошибка отправки уведомления об активации промокода: {e}")
return False
async def send_campaign_click_notification(
self,
db: AsyncSession,
campaign: AdvertisingCampaign,
telegram_user: types.User,
existing_user: Optional[User] = None
) -> bool:
if not self._is_enabled():
return False
try:
promo_group_info = await self._format_user_promo_group_info(db, existing_user)
if existing_user:
referrer_info = await self._get_referrer_info(db, existing_user.referred_by_id)
user_state = "✅ Зарегистрирован"
else:
referrer_info = ""
user_state = "🆕 Новый (не в базе)"
username_display = f"@{telegram_user.username}" if telegram_user.username else "отсутствует"
message = f"""📣 <b>ПЕРЕХОД ПО РЕКЛАМНОЙ КАМПАНИИ</b>
🔗 <b>Кампания:</b> {campaign.name} (ID: {campaign.id})
🧷 <b>Старт-параметр:</b> <code>{campaign.start_parameter}</code>
👤 <b>Пользователь:</b> {telegram_user.full_name or ''}
🆔 <b>Telegram ID:</b> <code>{telegram_user.id}</code>
📱 <b>Username:</b> {username_display}
📊 <b>Статус:</b> {user_state}
{promo_group_info}
🔗 <b>Реферер:</b> {referrer_info}
⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
return await self._send_message(message)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о переходе по кампании: {e}")
return False
async def send_promo_group_change_notification(
self,
db: AsyncSession,
user: User,
old_group: Optional[PromoGroup],
new_group: PromoGroup,
*,
changed_by: Optional[User] = None,
change_source: Optional[str] = None
) -> bool:
if not self._is_enabled():
return False
try:
promo_group_info = await self._format_user_promo_group_info(db, user)
referrer_info = await self._get_referrer_info(db, user.referred_by_id)
old_summary = self._format_promo_group_summary(old_group)
new_summary = self._format_promo_group_summary(new_group)
if changed_by:
changer_line = (
f"👨‍💼 <b>Инициатор:</b> {changed_by.full_name}"
f" (ID: <code>{changed_by.telegram_id}</code>)"
)
elif change_source == "auto":
changer_line = "🤖 <b>Инициатор:</b> Автоматическое назначение"
else:
changer_line = "ℹ️ <b>Инициатор:</b> Не указан"
message = f"""🏷️ <b>СМЕНА ПРОМОГРУППЫ</b>
👤 <b>Пользователь:</b> {user.full_name}
🆔 <b>Telegram ID:</b> <code>{user.telegram_id}</code>
📱 <b>Username:</b> @{user.username or 'отсутствует'}
{promo_group_info}
🔄 <b>Изменение:</b>
🔻 Была:
{old_summary}
🔺 Стала:
{new_summary}
{changer_line}
🔗 <b>Реферер:</b> {referrer_info}
⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
return await self._send_message(message)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о смене промогруппы: {e}")
return False
async def _format_servers_detailed(self, server_uuids: List[str]) -> str:
if not server_uuids:
return "Нет серверов"
@@ -827,6 +1011,65 @@ class AdminNotificationService:
return str(value)
return str(value)
async def _format_user_promo_group_info(
self,
db: AsyncSession,
user: Optional[User]
) -> str:
if not user:
return "🏷️ <b>Промогруппа:</b> не назначена\n💸 <b>Скидки:</b> недоступны"
try:
await db.refresh(user, ["promo_group"])
except Exception:
pass
promo_group: Optional[PromoGroup] = getattr(user, "promo_group", None)
if not promo_group:
return "🏷️ <b>Промогруппа:</b> не назначена\n💸 <b>Скидки:</b> недоступны"
discount_lines = self._build_promo_group_discount_lines(promo_group)
return "\n".join([
f"🏷️ <b>Промогруппа:</b> {promo_group.name}",
"💸 <b>Скидки:</b>",
*discount_lines,
])
def _format_promo_group_summary(self, promo_group: Optional[PromoGroup]) -> str:
if not promo_group:
return "— Не назначена"
lines = [f"{promo_group.name}"]
discount_lines = self._build_promo_group_discount_lines(promo_group)
lines.extend(f" {line}" for line in discount_lines)
return "\n".join(lines)
def _build_promo_group_discount_lines(self, promo_group: PromoGroup) -> List[str]:
period_text = self._format_period_discounts(promo_group)
return [
f"• Серверы: {promo_group.server_discount_percent}%",
f"• Трафик: {promo_group.traffic_discount_percent}%",
f"• Устройства: {promo_group.device_discount_percent}%",
f"• Периоды: {period_text}",
f"• Доп. услуги: {'Да' if promo_group.apply_discounts_to_addons else 'Нет'}",
]
def _format_period_discounts(self, promo_group: PromoGroup) -> str:
try:
discounts_map = promo_group._get_period_discounts_map()
except Exception:
discounts_map = {}
if not discounts_map:
return "нет"
parts = [
f"{days} д: {percent}%"
for days, percent in sorted(discounts_map.items(), key=lambda item: item[0])
]
return ", ".join(parts)
async def send_ticket_event_notification(
self,
text: str,
+2 -1
View File
@@ -65,7 +65,8 @@ class PromoCodeService:
return {
"success": True,
"description": result_description
"description": result_description,
"promocode": promocode,
}
except Exception as e: