diff --git a/app/services/admin_notification_service.py b/app/services/admin_notification_service.py index 5da6c2cb..de7407b9 100644 --- a/app/services/admin_notification_service.py +++ b/app/services/admin_notification_service.py @@ -77,6 +77,20 @@ class AdminNotificationService: ) return None + def _get_user_display(self, user: User) -> str: + first_name = getattr(user, "first_name", "") or "" + if first_name: + return first_name + + username = getattr(user, "username", "") or "" + if username: + return username + + telegram_id = getattr(user, "telegram_id", None) + if telegram_id is None: + return "IDUnknown" + return f"ID{telegram_id}" + def _format_promo_group_discounts(self, promo_group: PromoGroup) -> List[str]: discount_lines: List[str] = [] @@ -185,6 +199,7 @@ class AdminNotificationService: referrer_info = await self._get_referrer_info(db, user.referred_by_id) promo_group = await self._get_user_promo_group(db, user) promo_block = self._format_promo_group_block(promo_group) + user_display = self._get_user_display(user) trial_device_limit = subscription.device_limit if trial_device_limit is None: @@ -196,7 +211,7 @@ class AdminNotificationService: message = f"""🎯 АКТИВАЦИЯ ТРИАЛА -👤 Пользователь: {getattr(user, 'first_name', '') or getattr(user, 'username', '') or f"ID{getattr(user, 'telegram_id', 'Unknown')}"} +👤 Пользователь: {user_display} 🆔 Telegram ID: {user.telegram_id} 📱 Username: @{getattr(user, 'username', None) or 'отсутствует'} 👥 Статус: {user_status} @@ -235,26 +250,27 @@ class AdminNotificationService: try: event_type = "🔄 КОНВЕРСИЯ ИЗ ТРИАЛА" if was_trial_conversion else "💎 ПОКУПКА ПОДПИСКИ" - + if was_trial_conversion: user_status = "🎯 Конверсия из триала" elif user.has_had_paid_subscription: 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) if transaction else "Баланс" referrer_info = await self._get_referrer_info(db, user.referred_by_id) promo_group = await self._get_user_promo_group(db, user) promo_block = self._format_promo_group_block(promo_group) + user_display = self._get_user_display(user) total_amount = amount_kopeks if amount_kopeks is not None else (transaction.amount_kopeks if transaction else 0) transaction_id = transaction.id if transaction else "—" message = f"""💎 {event_type} -👤 Пользователь: {getattr(user, 'first_name', '') or getattr(user, 'username', '') or f"ID{getattr(user, 'telegram_id', 'Unknown')}"} +👤 Пользователь: {user_display} 🆔 Telegram ID: {user.telegram_id} 📱 Username: @{getattr(user, 'username', None) or 'отсутствует'} 👥 Статус: {user_status} @@ -372,10 +388,11 @@ class AdminNotificationService: subscription_status = self._get_subscription_status(subscription) promo_block = self._format_promo_group_block(promo_group) timestamp = datetime.now().strftime('%d.%m.%Y %H:%M:%S') + user_display = self._get_user_display(user) return f"""💰 ПОПОЛНЕНИЕ БАЛАНСА -👤 Пользователь: {getattr(user, 'first_name', '') or getattr(user, 'username', '') or f"ID{getattr(user, 'telegram_id', 'Unknown')}"} +👤 Пользователь: {user_display} 🆔 Telegram ID: {user.telegram_id} 📱 Username: @{getattr(user, 'username', None) or 'отсутствует'} 💳 Статус: {topup_status} @@ -548,13 +565,14 @@ class AdminNotificationService: servers_info = await self._get_servers_info(subscription.connected_squads) promo_group = await self._get_user_promo_group(db, user) promo_block = self._format_promo_group_block(promo_group) + user_display = self._get_user_display(user) current_end_date = new_end_date or subscription.end_date current_balance = balance_after if balance_after is not None else user.balance_kopeks message = f"""⏰ ПРОДЛЕНИЕ ПОДПИСКИ -👤 Пользователь: {getattr(user, 'first_name', '') or getattr(user, 'username', '') or f"ID{getattr(user, 'telegram_id', 'Unknown')}"} +👤 Пользователь: {user_display} 🆔 Telegram ID: {user.telegram_id} 📱 Username: @{getattr(user, 'username', None) or 'отсутствует'} @@ -600,11 +618,12 @@ class AdminNotificationService: promo_block = self._format_promo_group_block(promo_group) type_display = self._get_promocode_type_display(promocode_data.get("type")) usage_info = f"{promocode_data.get('current_uses', 0)}/{promocode_data.get('max_uses', 0)}" + user_display = self._get_user_display(user) message_lines = [ "🎫 АКТИВАЦИЯ ПРОМОКОДА", "", - f"👤 Пользователь: {getattr(user, 'first_name', '') or getattr(user, 'username', '') or f"ID{getattr(user, 'telegram_id', 'Unknown')}"}", + f"👤 Пользователь: {user_display}", f"🆔 Telegram ID: {user.telegram_id}", f"📱 Username: @{getattr(user, 'username', None) or 'отсутствует'}", "", @@ -727,11 +746,12 @@ class AdminNotificationService: ) elif automatic: initiator_line = "🤖 Автоматическое назначение" + user_display = self._get_user_display(user) message_lines = [ f"{title}", "", - f"👤 Пользователь: {getattr(user, 'first_name', '') or getattr(user, 'username', '') or f"ID{getattr(user, 'telegram_id', 'Unknown')}"}", + f"👤 Пользователь: {user_display}", f"🆔 Telegram ID: {user.telegram_id}", f"📱 Username: @{getattr(user, 'username', None) or 'отсутствует'}", "", @@ -1103,6 +1123,7 @@ class AdminNotificationService: referrer_info = await self._get_referrer_info(db, user.referred_by_id) promo_group = await self._get_user_promo_group(db, user) promo_block = self._format_promo_group_block(promo_group) + user_display = self._get_user_display(user) update_types = { "traffic": ("📊 ИЗМЕНЕНИЕ ТРАФИКА", "трафик"), @@ -1115,7 +1136,7 @@ class AdminNotificationService: message_lines = [ f"{title}", "", - f"👤 Пользователь: {getattr(user, 'first_name', '') or getattr(user, 'username', '') or f"ID{getattr(user, 'telegram_id', 'Unknown')}"}", + f"👤 Пользователь: {user_display}", f"🆔 Telegram ID: {user.telegram_id}", f"📱 Username: @{getattr(user, 'username', None) or 'отсутствует'}", "", diff --git a/app/webapi/routes/promo_groups.py b/app/webapi/routes/promo_groups.py index cec284d8..07ffc9e8 100644 --- a/app/webapi/routes/promo_groups.py +++ b/app/webapi/routes/promo_groups.py @@ -52,12 +52,12 @@ def _serialize(group: PromoGroup, members_count: int = 0) -> PromoGroupResponse: apply_discounts_to_addons=group.apply_discounts_to_addons, is_default=group.is_default, members_count=members_count, - created_at=group.created_at, - updated_at=group.updated_at, + created_at=getattr(group, "created_at", None), + updated_at=getattr(group, "updated_at", None), ) -@router.get("", response_model=PromoGroupListResponse) +@router.get("", response_model=PromoGroupListResponse, response_model_exclude_none=True) async def list_promo_groups( _: Any = Security(require_api_token), db: AsyncSession = Depends(get_db_session), @@ -79,7 +79,7 @@ async def list_promo_groups( ) -@router.get("/{group_id}", response_model=PromoGroupResponse) +@router.get("/{group_id}", response_model=PromoGroupResponse, response_model_exclude_none=True) async def get_promo_group( group_id: int, _: Any = Security(require_api_token), @@ -93,7 +93,12 @@ async def get_promo_group( return _serialize(group, members_count=members_count) -@router.post("", response_model=PromoGroupResponse, status_code=status.HTTP_201_CREATED) +@router.post( + "", + response_model=PromoGroupResponse, + response_model_exclude_none=True, + status_code=status.HTTP_201_CREATED, +) async def create_promo_group_endpoint( payload: PromoGroupCreateRequest, _: Any = Security(require_api_token), @@ -120,7 +125,11 @@ async def create_promo_group_endpoint( return _serialize(group, members_count=0) -@router.patch("/{group_id}", response_model=PromoGroupResponse) +@router.patch( + "/{group_id}", + response_model=PromoGroupResponse, + response_model_exclude_none=True, +) async def update_promo_group_endpoint( group_id: int, payload: PromoGroupUpdateRequest, diff --git a/app/webapi/schemas/promo_groups.py b/app/webapi/schemas/promo_groups.py index ffd2e68e..d006f9e7 100644 --- a/app/webapi/schemas/promo_groups.py +++ b/app/webapi/schemas/promo_groups.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime from typing import Dict, Optional -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, ConfigDict, Field, validator def _normalize_period_discounts(value: Optional[Dict[object, object]]) -> Optional[Dict[int, int]]: @@ -23,6 +23,8 @@ def _normalize_period_discounts(value: Optional[Dict[object, object]]) -> Option class PromoGroupResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + id: int name: str server_discount_percent: int @@ -33,8 +35,8 @@ class PromoGroupResponse(BaseModel): apply_discounts_to_addons: bool is_default: bool members_count: int = 0 - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None class _PromoGroupBase(BaseModel):