Files
remnawave-bedolaga-telegram…/app/webapi/routes/campaigns.py
c0mrade 9a2aea038a chore: add uv package manager and ruff linter configuration
- Add pyproject.toml with uv and ruff configuration
- Pin Python version to 3.13 via .python-version
- Add Makefile commands: lint, format, fix
- Apply ruff formatting to entire codebase
- Remove unused imports (base64 in yookassa/simple_subscription)
- Update .gitignore for new config files
2026-01-24 17:45:27 +03:00

179 lines
5.8 KiB
Python

from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, Security, status
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.crud.campaign import (
create_campaign,
delete_campaign,
get_campaign_by_id,
get_campaigns_count,
get_campaigns_list,
update_campaign,
)
from ..dependencies import get_db_session, require_api_token
from ..schemas.campaigns import (
CampaignCreateRequest,
CampaignListResponse,
CampaignResponse,
CampaignUpdateRequest,
)
router = APIRouter()
def _serialize_campaign(campaign) -> CampaignResponse:
registrations_attr = None
if isinstance(getattr(campaign, '__dict__', None), dict):
registrations_attr = campaign.__dict__.get('registrations')
registrations = registrations_attr or []
squads = list(campaign.subscription_squads or [])
# Получаем название тарифа если есть
tariff_name = None
if campaign.tariff_id and hasattr(campaign, 'tariff') and campaign.tariff:
tariff_name = campaign.tariff.name
return CampaignResponse(
id=campaign.id,
name=campaign.name,
start_parameter=campaign.start_parameter,
bonus_type=campaign.bonus_type,
balance_bonus_kopeks=campaign.balance_bonus_kopeks or 0,
balance_bonus_rubles=round((campaign.balance_bonus_kopeks or 0) / 100, 2),
subscription_duration_days=campaign.subscription_duration_days,
subscription_traffic_gb=campaign.subscription_traffic_gb,
subscription_device_limit=campaign.subscription_device_limit,
subscription_squads=squads,
tariff_id=campaign.tariff_id,
tariff_duration_days=campaign.tariff_duration_days,
tariff_name=tariff_name,
is_active=campaign.is_active,
created_by=campaign.created_by,
created_at=campaign.created_at,
updated_at=campaign.updated_at,
registrations_count=len(registrations),
)
@router.post(
'',
response_model=CampaignResponse,
status_code=status.HTTP_201_CREATED,
summary='Создать рекламную кампанию',
)
async def create_campaign_endpoint(
payload: CampaignCreateRequest,
token: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> CampaignResponse:
created_by = getattr(token, 'id', None)
try:
campaign = await create_campaign(
db,
name=payload.name,
start_parameter=payload.start_parameter,
bonus_type=payload.bonus_type,
created_by=created_by,
balance_bonus_kopeks=payload.balance_bonus_kopeks,
subscription_duration_days=payload.subscription_duration_days,
subscription_traffic_gb=payload.subscription_traffic_gb,
subscription_device_limit=payload.subscription_device_limit,
subscription_squads=payload.subscription_squads,
tariff_id=payload.tariff_id,
tariff_duration_days=payload.tariff_duration_days,
is_active=payload.is_active,
)
except IntegrityError as exc: # duplicate start_parameter
await db.rollback()
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
'Campaign with this start_parameter already exists',
) from exc
return _serialize_campaign(campaign)
@router.get(
'',
response_model=CampaignListResponse,
summary='Список рекламных кампаний',
)
async def list_campaigns(
_: 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='Включать неактивные кампании'),
) -> CampaignListResponse:
total = await get_campaigns_count(db, is_active=None if include_inactive else True)
campaigns = await get_campaigns_list(
db,
offset=offset,
limit=limit,
include_inactive=include_inactive,
)
return CampaignListResponse(
items=[_serialize_campaign(campaign) for campaign in campaigns],
total=total,
limit=limit,
offset=offset,
)
@router.delete(
'/{campaign_id}',
status_code=status.HTTP_204_NO_CONTENT,
summary='Удалить рекламную кампанию',
)
async def delete_campaign_endpoint(
campaign_id: int,
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
):
campaign = await get_campaign_by_id(db, campaign_id)
if not campaign:
raise HTTPException(status.HTTP_404_NOT_FOUND, 'Campaign not found')
await delete_campaign(db, campaign)
@router.patch(
'/{campaign_id}',
response_model=CampaignResponse,
summary='Обновить рекламную кампанию',
)
async def update_campaign_endpoint(
campaign_id: int,
payload: CampaignUpdateRequest,
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> CampaignResponse:
campaign = await get_campaign_by_id(db, campaign_id)
if not campaign:
raise HTTPException(status.HTTP_404_NOT_FOUND, 'Campaign not found')
update_fields = payload.dict(exclude_unset=True)
if not update_fields:
return _serialize_campaign(campaign)
try:
campaign = await update_campaign(db, campaign, **update_fields)
except IntegrityError as exc:
await db.rollback()
if 'start_parameter' in str(exc.orig):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
'Campaign with this start_parameter already exists',
) from exc
raise
return _serialize_campaign(campaign)