mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Merge pull request #2038 from BEDOLAGA-DEV/dev5
Апи для сообщений в меню и приветственного текста и доп уведомления
This commit is contained in:
@@ -5,7 +5,7 @@ from typing import Optional, List
|
||||
from sqlalchemy import select, func, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import UserMessage
|
||||
from app.database.models import User, UserMessage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -13,15 +13,21 @@ logger = logging.getLogger(__name__)
|
||||
async def create_user_message(
|
||||
db: AsyncSession,
|
||||
message_text: str,
|
||||
created_by: int,
|
||||
created_by: Optional[int] = None,
|
||||
is_active: bool = True,
|
||||
sort_order: int = 0
|
||||
) -> UserMessage:
|
||||
resolved_creator = created_by
|
||||
|
||||
if created_by is not None:
|
||||
result = await db.execute(select(User.id).where(User.id == created_by))
|
||||
resolved_creator = result.scalar_one_or_none()
|
||||
|
||||
message = UserMessage(
|
||||
message_text=message_text,
|
||||
is_active=is_active,
|
||||
sort_order=sort_order,
|
||||
created_by=created_by
|
||||
created_by=resolved_creator,
|
||||
)
|
||||
|
||||
db.add(message)
|
||||
@@ -61,19 +67,27 @@ async def get_random_active_message(db: AsyncSession) -> Optional[str]:
|
||||
async def get_all_user_messages(
|
||||
db: AsyncSession,
|
||||
offset: int = 0,
|
||||
limit: int = 50
|
||||
limit: int = 50,
|
||||
include_inactive: bool = True,
|
||||
) -> List[UserMessage]:
|
||||
query = select(UserMessage).order_by(UserMessage.created_at.desc())
|
||||
if not include_inactive:
|
||||
query = query.where(UserMessage.is_active == True)
|
||||
|
||||
result = await db.execute(
|
||||
select(UserMessage)
|
||||
.order_by(UserMessage.created_at.desc())
|
||||
query
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def get_user_messages_count(db: AsyncSession) -> int:
|
||||
result = await db.execute(select(func.count(UserMessage.id)))
|
||||
async def get_user_messages_count(db: AsyncSession, include_inactive: bool = True) -> int:
|
||||
query = select(func.count(UserMessage.id))
|
||||
if not include_inactive:
|
||||
query = query.where(UserMessage.is_active == True)
|
||||
|
||||
result = await db.execute(query)
|
||||
return result.scalar()
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy import select, update, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import WelcomeText
|
||||
from app.database.models import User, WelcomeText
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,6 +45,37 @@ async def get_current_welcome_text_settings(db: AsyncSession) -> dict:
|
||||
'id': None
|
||||
}
|
||||
|
||||
|
||||
async def get_welcome_text_by_id(db: AsyncSession, welcome_text_id: int) -> Optional[WelcomeText]:
|
||||
result = await db.execute(
|
||||
select(WelcomeText).where(WelcomeText.id == welcome_text_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_welcome_texts(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
include_inactive: bool = True,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
):
|
||||
query = select(WelcomeText).order_by(WelcomeText.updated_at.desc())
|
||||
if not include_inactive:
|
||||
query = query.where(WelcomeText.is_active == True)
|
||||
|
||||
result = await db.execute(query.limit(limit).offset(offset))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def count_welcome_texts(db: AsyncSession, *, include_inactive: bool = True) -> int:
|
||||
query = select(func.count(WelcomeText.id))
|
||||
if not include_inactive:
|
||||
query = query.where(WelcomeText.is_active == True)
|
||||
|
||||
result = await db.execute(query)
|
||||
return result.scalar()
|
||||
|
||||
async def toggle_welcome_text_status(db: AsyncSession, admin_id: int) -> bool:
|
||||
try:
|
||||
result = await db.execute(
|
||||
@@ -113,6 +144,87 @@ async def set_welcome_text(db: AsyncSession, text_content: str, admin_id: int) -
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
async def create_welcome_text(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
text_content: str,
|
||||
created_by: Optional[int] = None,
|
||||
is_enabled: bool = True,
|
||||
is_active: bool = True,
|
||||
) -> WelcomeText:
|
||||
resolved_creator = created_by
|
||||
|
||||
if created_by is not None:
|
||||
result = await db.execute(select(User.id).where(User.id == created_by))
|
||||
resolved_creator = result.scalar_one_or_none()
|
||||
|
||||
if is_active:
|
||||
await db.execute(update(WelcomeText).values(is_active=False))
|
||||
|
||||
welcome_text = WelcomeText(
|
||||
text_content=text_content,
|
||||
is_active=is_active,
|
||||
is_enabled=is_enabled,
|
||||
created_by=resolved_creator,
|
||||
)
|
||||
|
||||
db.add(welcome_text)
|
||||
await db.commit()
|
||||
await db.refresh(welcome_text)
|
||||
|
||||
logger.info(
|
||||
"✅ Создан приветственный текст ID %s (активный=%s, включен=%s)",
|
||||
welcome_text.id,
|
||||
welcome_text.is_active,
|
||||
welcome_text.is_enabled,
|
||||
)
|
||||
return welcome_text
|
||||
|
||||
|
||||
async def update_welcome_text(
|
||||
db: AsyncSession,
|
||||
welcome_text: WelcomeText,
|
||||
*,
|
||||
text_content: Optional[str] = None,
|
||||
is_enabled: Optional[bool] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
) -> WelcomeText:
|
||||
if is_active:
|
||||
await db.execute(
|
||||
update(WelcomeText)
|
||||
.where(WelcomeText.id != welcome_text.id)
|
||||
.values(is_active=False)
|
||||
)
|
||||
|
||||
if text_content is not None:
|
||||
welcome_text.text_content = text_content
|
||||
|
||||
if is_enabled is not None:
|
||||
welcome_text.is_enabled = is_enabled
|
||||
|
||||
if is_active is not None:
|
||||
welcome_text.is_active = is_active
|
||||
|
||||
welcome_text.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(welcome_text)
|
||||
|
||||
logger.info(
|
||||
"📝 Обновлен приветственный текст ID %s (активный=%s, включен=%s)",
|
||||
welcome_text.id,
|
||||
welcome_text.is_active,
|
||||
welcome_text.is_enabled,
|
||||
)
|
||||
return welcome_text
|
||||
|
||||
|
||||
async def delete_welcome_text(db: AsyncSession, welcome_text: WelcomeText) -> None:
|
||||
await db.delete(welcome_text)
|
||||
await db.commit()
|
||||
logger.info("🗑️ Удален приветственный текст ID %s", welcome_text.id)
|
||||
|
||||
async def get_current_welcome_text_or_default() -> str:
|
||||
return (
|
||||
f"Привет, {{user_name}}! 🎁 3 дней VPN бесплатно! "
|
||||
|
||||
@@ -99,7 +99,7 @@ class AdminNotificationService:
|
||||
*,
|
||||
event_type: str,
|
||||
user: User,
|
||||
subscription: Subscription,
|
||||
subscription: Subscription | None,
|
||||
transaction: Transaction | None = None,
|
||||
amount_kopeks: int | None = None,
|
||||
message: str | None = None,
|
||||
@@ -549,11 +549,38 @@ class AdminNotificationService:
|
||||
promo_group: PromoGroup | None,
|
||||
db: AsyncSession | None = None,
|
||||
) -> bool:
|
||||
logger.info("Начинаем отправку уведомления о пополнении баланса")
|
||||
|
||||
if db:
|
||||
try:
|
||||
await self._record_subscription_event(
|
||||
db,
|
||||
event_type="balance_topup",
|
||||
user=user,
|
||||
subscription=subscription,
|
||||
transaction=transaction,
|
||||
amount_kopeks=transaction.amount_kopeks,
|
||||
message="Balance top-up",
|
||||
occurred_at=transaction.completed_at or transaction.created_at,
|
||||
extra={
|
||||
"status": topup_status,
|
||||
"balance_before": old_balance,
|
||||
"balance_after": user.balance_kopeks,
|
||||
"referrer_info": referrer_info,
|
||||
"promo_group_id": getattr(promo_group, "id", None),
|
||||
"promo_group_name": getattr(promo_group, "name", None),
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.error(
|
||||
"Не удалось сохранить событие пополнения баланса пользователя %s",
|
||||
getattr(user, "id", "unknown"),
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
logger.info("Начинаем отправку уведомления о пополнении баланса")
|
||||
|
||||
try:
|
||||
logger.info("Пытаемся создать сообщение уведомления")
|
||||
message = self._build_balance_topup_message(
|
||||
@@ -718,6 +745,36 @@ class AdminNotificationService:
|
||||
promocode_data: Dict[str, Any],
|
||||
effect_description: str,
|
||||
) -> bool:
|
||||
try:
|
||||
await self._record_subscription_event(
|
||||
db,
|
||||
event_type="promocode_activation",
|
||||
user=user,
|
||||
subscription=None,
|
||||
transaction=None,
|
||||
amount_kopeks=promocode_data.get("balance_bonus_kopeks"),
|
||||
message="Promocode activation",
|
||||
occurred_at=datetime.utcnow(),
|
||||
extra={
|
||||
"code": promocode_data.get("code"),
|
||||
"type": promocode_data.get("type"),
|
||||
"subscription_days": promocode_data.get("subscription_days"),
|
||||
"balance_bonus_kopeks": promocode_data.get("balance_bonus_kopeks"),
|
||||
"description": effect_description,
|
||||
"valid_until": (
|
||||
promocode_data.get("valid_until").isoformat()
|
||||
if isinstance(promocode_data.get("valid_until"), datetime)
|
||||
else promocode_data.get("valid_until")
|
||||
),
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.error(
|
||||
"Не удалось сохранить событие активации промокода пользователя %s",
|
||||
getattr(user, "id", "unknown"),
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
@@ -784,6 +841,31 @@ class AdminNotificationService:
|
||||
campaign: AdvertisingCampaign,
|
||||
user: Optional[User] = None,
|
||||
) -> bool:
|
||||
if user:
|
||||
try:
|
||||
await self._record_subscription_event(
|
||||
db,
|
||||
event_type="referral_link_visit",
|
||||
user=user,
|
||||
subscription=None,
|
||||
transaction=None,
|
||||
amount_kopeks=None,
|
||||
message="Referral link visit",
|
||||
occurred_at=datetime.utcnow(),
|
||||
extra={
|
||||
"campaign_id": campaign.id,
|
||||
"campaign_name": campaign.name,
|
||||
"start_parameter": campaign.start_parameter,
|
||||
"was_registered": bool(user),
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.error(
|
||||
"Не удалось сохранить событие перехода по кампании для пользователя %s",
|
||||
getattr(user, "id", "unknown"),
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
@@ -842,6 +924,33 @@ class AdminNotificationService:
|
||||
initiator: Optional[User] = None,
|
||||
automatic: bool = False,
|
||||
) -> bool:
|
||||
try:
|
||||
await self._record_subscription_event(
|
||||
db,
|
||||
event_type="promo_group_change",
|
||||
user=user,
|
||||
subscription=None,
|
||||
transaction=None,
|
||||
message="Promo group change",
|
||||
occurred_at=datetime.utcnow(),
|
||||
extra={
|
||||
"old_group_id": getattr(old_group, "id", None),
|
||||
"old_group_name": getattr(old_group, "name", None),
|
||||
"new_group_id": new_group.id,
|
||||
"new_group_name": new_group.name,
|
||||
"reason": reason,
|
||||
"initiator_id": getattr(initiator, "id", None),
|
||||
"initiator_telegram_id": getattr(initiator, "telegram_id", None),
|
||||
"automatic": automatic,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.error(
|
||||
"Не удалось сохранить событие смены промогруппы пользователя %s",
|
||||
getattr(user, "id", "unknown"),
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ from .routes import (
|
||||
promocodes,
|
||||
promo_groups,
|
||||
promo_offers,
|
||||
user_messages,
|
||||
welcome_texts,
|
||||
pages,
|
||||
remnawave,
|
||||
servers,
|
||||
@@ -49,7 +51,11 @@ OPENAPI_TAGS = [
|
||||
},
|
||||
{
|
||||
"name": "main-menu",
|
||||
"description": "Управление кнопками главного меню Telegram-бота.",
|
||||
"description": "Управление кнопками и сообщениями главного меню Telegram-бота.",
|
||||
},
|
||||
{
|
||||
"name": "welcome-texts",
|
||||
"description": "Создание, редактирование и управление приветственными текстами.",
|
||||
},
|
||||
{
|
||||
"name": "users",
|
||||
@@ -122,8 +128,9 @@ OPENAPI_TAGS = [
|
||||
{
|
||||
"name": "notifications",
|
||||
"description": (
|
||||
"Получение и просмотр уведомлений о покупках, активациях и продлениях подписок "
|
||||
"для административной панели."
|
||||
"Получение и просмотр уведомлений о покупках, активациях и продлениях подписок, "
|
||||
"пополнениях баланса, активациях промокодов, переходах по реферальным ссылкам и "
|
||||
"сменах промогрупп пользователей для административной панели."
|
||||
),
|
||||
},
|
||||
]
|
||||
@@ -169,6 +176,16 @@ def create_web_api_app() -> FastAPI:
|
||||
prefix="/main-menu/buttons",
|
||||
tags=["main-menu"],
|
||||
)
|
||||
app.include_router(
|
||||
user_messages.router,
|
||||
prefix="/main-menu/messages",
|
||||
tags=["main-menu"],
|
||||
)
|
||||
app.include_router(
|
||||
welcome_texts.router,
|
||||
prefix="/welcome-texts",
|
||||
tags=["welcome-texts"],
|
||||
)
|
||||
app.include_router(pages.router, prefix="/pages", tags=["pages"])
|
||||
app.include_router(promocodes.router, prefix="/promo-codes", tags=["promo-codes"])
|
||||
app.include_router(broadcasts.router, prefix="/broadcasts", tags=["broadcasts"])
|
||||
|
||||
@@ -7,6 +7,8 @@ from . import (
|
||||
partners,
|
||||
polls,
|
||||
promo_offers,
|
||||
user_messages,
|
||||
welcome_texts,
|
||||
pages,
|
||||
promo_groups,
|
||||
servers,
|
||||
@@ -30,6 +32,8 @@ __all__ = [
|
||||
"partners",
|
||||
"polls",
|
||||
"promo_offers",
|
||||
"user_messages",
|
||||
"welcome_texts",
|
||||
"pages",
|
||||
"promo_groups",
|
||||
"servers",
|
||||
|
||||
124
app/webapi/routes/user_messages.py
Normal file
124
app/webapi/routes/user_messages.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, Security, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.crud.user_message import (
|
||||
create_user_message,
|
||||
delete_user_message,
|
||||
get_all_user_messages,
|
||||
get_user_message_by_id,
|
||||
get_user_messages_count,
|
||||
toggle_user_message_status,
|
||||
update_user_message,
|
||||
)
|
||||
|
||||
from ..dependencies import get_db_session, require_api_token
|
||||
from ..schemas.user_messages import (
|
||||
UserMessageCreateRequest,
|
||||
UserMessageListResponse,
|
||||
UserMessageResponse,
|
||||
UserMessageUpdateRequest,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _serialize(message) -> UserMessageResponse:
|
||||
return UserMessageResponse(
|
||||
id=message.id,
|
||||
message_text=message.message_text,
|
||||
is_active=message.is_active,
|
||||
sort_order=message.sort_order,
|
||||
created_by=message.created_by,
|
||||
created_at=message.created_at,
|
||||
updated_at=message.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=UserMessageListResponse)
|
||||
async def list_user_messages(
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
include_inactive: bool = Query(True, description="Включать неактивные сообщения"),
|
||||
) -> UserMessageListResponse:
|
||||
total = await get_user_messages_count(db, include_inactive=include_inactive)
|
||||
messages = await get_all_user_messages(
|
||||
db,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
include_inactive=include_inactive,
|
||||
)
|
||||
|
||||
return UserMessageListResponse(
|
||||
items=[_serialize(message) for message in messages],
|
||||
total=total,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=UserMessageResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user_message_endpoint(
|
||||
payload: UserMessageCreateRequest,
|
||||
token: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> UserMessageResponse:
|
||||
created_by = getattr(token, "id", None)
|
||||
message = await create_user_message(
|
||||
db,
|
||||
message_text=payload.message_text,
|
||||
created_by=created_by,
|
||||
is_active=payload.is_active,
|
||||
sort_order=payload.sort_order,
|
||||
)
|
||||
|
||||
return _serialize(message)
|
||||
|
||||
|
||||
@router.patch("/{message_id}", response_model=UserMessageResponse)
|
||||
async def update_user_message_endpoint(
|
||||
message_id: int,
|
||||
payload: UserMessageUpdateRequest,
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> UserMessageResponse:
|
||||
update_payload = payload.dict(exclude_unset=True)
|
||||
message = await update_user_message(db, message_id, **update_payload)
|
||||
|
||||
if not message:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "User message not found")
|
||||
|
||||
return _serialize(message)
|
||||
|
||||
|
||||
@router.post("/{message_id}/toggle", response_model=UserMessageResponse)
|
||||
async def toggle_user_message_endpoint(
|
||||
message_id: int,
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> UserMessageResponse:
|
||||
message = await toggle_user_message_status(db, message_id)
|
||||
|
||||
if not message:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "User message not found")
|
||||
|
||||
return _serialize(message)
|
||||
|
||||
|
||||
@router.delete("/{message_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user_message_endpoint(
|
||||
message_id: int,
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> Response:
|
||||
message = await get_user_message_by_id(db, message_id)
|
||||
if not message:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "User message not found")
|
||||
|
||||
await delete_user_message(db, message_id)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
124
app/webapi/routes/welcome_texts.py
Normal file
124
app/webapi/routes/welcome_texts.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, Security, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.crud.welcome_text import (
|
||||
count_welcome_texts,
|
||||
create_welcome_text,
|
||||
delete_welcome_text,
|
||||
get_welcome_text_by_id,
|
||||
list_welcome_texts,
|
||||
update_welcome_text,
|
||||
)
|
||||
|
||||
from ..dependencies import get_db_session, require_api_token
|
||||
from ..schemas.welcome_texts import (
|
||||
WelcomeTextCreateRequest,
|
||||
WelcomeTextListResponse,
|
||||
WelcomeTextResponse,
|
||||
WelcomeTextUpdateRequest,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _serialize(text) -> WelcomeTextResponse:
|
||||
return WelcomeTextResponse(
|
||||
id=text.id,
|
||||
text=text.text_content,
|
||||
is_active=text.is_active,
|
||||
is_enabled=text.is_enabled,
|
||||
created_by=text.created_by,
|
||||
created_at=text.created_at,
|
||||
updated_at=text.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=WelcomeTextListResponse)
|
||||
async def list_welcome_texts_endpoint(
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
include_inactive: bool = Query(True, description="Включать неактивные тексты"),
|
||||
) -> WelcomeTextListResponse:
|
||||
total = await count_welcome_texts(db, include_inactive=include_inactive)
|
||||
records = await list_welcome_texts(
|
||||
db,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
include_inactive=include_inactive,
|
||||
)
|
||||
|
||||
return WelcomeTextListResponse(
|
||||
items=[_serialize(item) for item in records],
|
||||
total=total,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=WelcomeTextResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_welcome_text_endpoint(
|
||||
payload: WelcomeTextCreateRequest,
|
||||
token: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> WelcomeTextResponse:
|
||||
created_by = getattr(token, "id", None)
|
||||
record = await create_welcome_text(
|
||||
db,
|
||||
text_content=payload.text,
|
||||
created_by=created_by,
|
||||
is_enabled=payload.is_enabled,
|
||||
is_active=payload.is_active,
|
||||
)
|
||||
|
||||
return _serialize(record)
|
||||
|
||||
|
||||
@router.get("/{welcome_text_id}", response_model=WelcomeTextResponse)
|
||||
async def get_welcome_text_endpoint(
|
||||
welcome_text_id: int,
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> WelcomeTextResponse:
|
||||
record = await get_welcome_text_by_id(db, welcome_text_id)
|
||||
if not record:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Welcome text not found")
|
||||
|
||||
return _serialize(record)
|
||||
|
||||
|
||||
@router.patch("/{welcome_text_id}", response_model=WelcomeTextResponse)
|
||||
async def update_welcome_text_endpoint(
|
||||
welcome_text_id: int,
|
||||
payload: WelcomeTextUpdateRequest,
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> WelcomeTextResponse:
|
||||
record = await get_welcome_text_by_id(db, welcome_text_id)
|
||||
if not record:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Welcome text not found")
|
||||
|
||||
update_payload = payload.dict(exclude_unset=True)
|
||||
if "text" in update_payload:
|
||||
update_payload["text_content"] = update_payload.pop("text")
|
||||
updated = await update_welcome_text(db, record, **update_payload)
|
||||
return _serialize(updated)
|
||||
|
||||
|
||||
@router.delete("/{welcome_text_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_welcome_text_endpoint(
|
||||
welcome_text_id: int,
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> Response:
|
||||
record = await get_welcome_text_by_id(db, welcome_text_id)
|
||||
if not record:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Welcome text not found")
|
||||
|
||||
await delete_welcome_text(db, record)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
@@ -7,7 +7,15 @@ from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class SubscriptionEventCreate(BaseModel):
|
||||
event_type: Literal["activation", "purchase", "renewal"]
|
||||
event_type: Literal[
|
||||
"activation",
|
||||
"purchase",
|
||||
"renewal",
|
||||
"balance_topup",
|
||||
"promocode_activation",
|
||||
"referral_link_visit",
|
||||
"promo_group_change",
|
||||
]
|
||||
user_id: int = Field(..., ge=1)
|
||||
subscription_id: Optional[int] = Field(default=None, ge=1)
|
||||
transaction_id: Optional[int] = Field(default=None, ge=1)
|
||||
|
||||
50
app/webapi/schemas/user_messages.py
Normal file
50
app/webapi/schemas/user_messages.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
|
||||
def _normalize_text(value: str) -> str:
|
||||
cleaned = (value or "").strip()
|
||||
if not cleaned:
|
||||
raise ValueError("Message text cannot be empty")
|
||||
return cleaned
|
||||
|
||||
|
||||
class UserMessageResponse(BaseModel):
|
||||
id: int
|
||||
message_text: str
|
||||
is_active: bool
|
||||
sort_order: int
|
||||
created_by: Optional[int]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class UserMessageCreateRequest(BaseModel):
|
||||
message_text: str = Field(..., min_length=1, max_length=4000)
|
||||
is_active: bool = True
|
||||
sort_order: int = Field(0, ge=0)
|
||||
|
||||
_normalize_message_text = validator("message_text", allow_reuse=True)(_normalize_text)
|
||||
|
||||
|
||||
class UserMessageUpdateRequest(BaseModel):
|
||||
message_text: Optional[str] = Field(None, min_length=1, max_length=4000)
|
||||
is_active: Optional[bool] = None
|
||||
sort_order: Optional[int] = Field(None, ge=0)
|
||||
|
||||
@validator("message_text")
|
||||
def validate_message_text(cls, value): # noqa: D401,B902
|
||||
if value is None:
|
||||
return value
|
||||
return _normalize_text(value)
|
||||
|
||||
|
||||
class UserMessageListResponse(BaseModel):
|
||||
items: list[UserMessageResponse]
|
||||
total: int
|
||||
limit: int
|
||||
offset: int
|
||||
50
app/webapi/schemas/welcome_texts.py
Normal file
50
app/webapi/schemas/welcome_texts.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
|
||||
def _normalize_text(value: str) -> str:
|
||||
cleaned = (value or "").strip()
|
||||
if not cleaned:
|
||||
raise ValueError("Text cannot be empty")
|
||||
return cleaned
|
||||
|
||||
|
||||
class WelcomeTextResponse(BaseModel):
|
||||
id: int
|
||||
text: str
|
||||
is_active: bool
|
||||
is_enabled: bool
|
||||
created_by: Optional[int]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class WelcomeTextCreateRequest(BaseModel):
|
||||
text: str = Field(..., min_length=1, max_length=4000)
|
||||
is_enabled: bool = True
|
||||
is_active: bool = True
|
||||
|
||||
_normalize_text = validator("text", allow_reuse=True)(_normalize_text)
|
||||
|
||||
|
||||
class WelcomeTextUpdateRequest(BaseModel):
|
||||
text: Optional[str] = Field(None, min_length=1, max_length=4000)
|
||||
is_enabled: Optional[bool] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
@validator("text")
|
||||
def validate_text(cls, value): # noqa: D401,B902
|
||||
if value is None:
|
||||
return value
|
||||
return _normalize_text(value)
|
||||
|
||||
|
||||
class WelcomeTextListResponse(BaseModel):
|
||||
items: list[WelcomeTextResponse]
|
||||
total: int
|
||||
limit: int
|
||||
offset: int
|
||||
Reference in New Issue
Block a user