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):