mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
510 lines
18 KiB
Python
510 lines
18 KiB
Python
"""Admin routes for managing advertising campaigns in cabinet."""
|
|
|
|
import logging
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func
|
|
|
|
from app.config import settings
|
|
from app.database.models import (
|
|
User,
|
|
AdvertisingCampaign,
|
|
AdvertisingCampaignRegistration,
|
|
Subscription,
|
|
Tariff,
|
|
)
|
|
from app.database.crud.campaign import (
|
|
create_campaign,
|
|
delete_campaign,
|
|
get_campaign_by_id,
|
|
get_campaign_by_start_parameter,
|
|
get_campaign_statistics,
|
|
get_campaigns_count,
|
|
get_campaigns_list,
|
|
get_campaigns_overview,
|
|
update_campaign,
|
|
)
|
|
from app.database.crud.server_squad import get_all_server_squads
|
|
from app.database.crud.tariff import get_all_tariffs
|
|
|
|
from ..dependencies import get_cabinet_db, get_current_admin_user
|
|
from ..schemas.campaigns import (
|
|
CampaignListResponse,
|
|
CampaignListItem,
|
|
CampaignDetailResponse,
|
|
CampaignCreateRequest,
|
|
CampaignUpdateRequest,
|
|
CampaignToggleResponse,
|
|
CampaignStatisticsResponse,
|
|
CampaignRegistrationItem,
|
|
CampaignRegistrationsResponse,
|
|
CampaignsOverviewResponse,
|
|
TariffInfo,
|
|
ServerSquadInfo,
|
|
)
|
|
from ..schemas.tariffs import TariffListItem
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/admin/campaigns", tags=["Cabinet Admin Campaigns"])
|
|
|
|
|
|
def _get_deep_link(start_parameter: str) -> str:
|
|
"""Generate deep link for campaign."""
|
|
bot_username = settings.get_bot_username()
|
|
if bot_username:
|
|
return f"https://t.me/{bot_username}?start={start_parameter}"
|
|
return f"?start={start_parameter}"
|
|
|
|
|
|
@router.get("/overview", response_model=CampaignsOverviewResponse)
|
|
async def get_overview(
|
|
admin: User = Depends(get_current_admin_user),
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
):
|
|
"""Get campaigns overview statistics."""
|
|
overview = await get_campaigns_overview(db)
|
|
|
|
# Count tariff bonuses
|
|
tariff_result = await db.execute(
|
|
select(func.count(AdvertisingCampaignRegistration.id)).where(
|
|
AdvertisingCampaignRegistration.bonus_type == "tariff"
|
|
)
|
|
)
|
|
tariff_count = tariff_result.scalar() or 0
|
|
|
|
return CampaignsOverviewResponse(
|
|
total=overview["total"],
|
|
active=overview["active"],
|
|
inactive=overview["inactive"],
|
|
total_registrations=overview["registrations"],
|
|
total_balance_issued_kopeks=overview["balance_total"],
|
|
total_balance_issued_rubles=overview["balance_total"] / 100,
|
|
total_subscription_issued=overview["subscription_total"],
|
|
total_tariff_issued=tariff_count,
|
|
)
|
|
|
|
|
|
@router.get("/available-servers", response_model=List[ServerSquadInfo])
|
|
async def get_available_servers(
|
|
admin: User = Depends(get_current_admin_user),
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
):
|
|
"""Get list of available server squads for campaign subscription bonus."""
|
|
servers, _ = await get_all_server_squads(db, available_only=False)
|
|
return [
|
|
ServerSquadInfo(
|
|
id=server.id,
|
|
squad_uuid=server.squad_uuid,
|
|
display_name=server.display_name,
|
|
country_code=server.country_code,
|
|
)
|
|
for server in servers
|
|
]
|
|
|
|
|
|
@router.get("/available-tariffs", response_model=List[TariffListItem])
|
|
async def get_available_tariffs(
|
|
admin: User = Depends(get_current_admin_user),
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
):
|
|
"""Get list of available tariffs for campaign tariff bonus."""
|
|
tariffs = await get_all_tariffs(db, include_inactive=False)
|
|
return [
|
|
TariffListItem(
|
|
id=tariff.id,
|
|
name=tariff.name,
|
|
description=tariff.description,
|
|
is_active=tariff.is_active,
|
|
is_trial_available=tariff.is_trial_available,
|
|
is_daily=tariff.is_daily,
|
|
daily_price_kopeks=tariff.daily_price_kopeks or 0,
|
|
allow_traffic_topup=tariff.allow_traffic_topup,
|
|
traffic_limit_gb=tariff.traffic_limit_gb,
|
|
device_limit=tariff.device_limit,
|
|
tier_level=tariff.tier_level,
|
|
display_order=tariff.display_order,
|
|
servers_count=len(tariff.allowed_squads or []),
|
|
subscriptions_count=0,
|
|
created_at=tariff.created_at,
|
|
)
|
|
for tariff in tariffs
|
|
]
|
|
|
|
|
|
@router.get("", response_model=CampaignListResponse)
|
|
async def list_campaigns(
|
|
include_inactive: bool = True,
|
|
offset: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=100),
|
|
admin: User = Depends(get_current_admin_user),
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
):
|
|
"""Get list of all campaigns."""
|
|
campaigns = await get_campaigns_list(
|
|
db, offset=offset, limit=limit, include_inactive=include_inactive
|
|
)
|
|
total = await get_campaigns_count(db)
|
|
|
|
items = []
|
|
for campaign in campaigns:
|
|
# Get quick stats
|
|
stats = await get_campaign_statistics(db, campaign.id)
|
|
items.append(CampaignListItem(
|
|
id=campaign.id,
|
|
name=campaign.name,
|
|
start_parameter=campaign.start_parameter,
|
|
bonus_type=campaign.bonus_type,
|
|
is_active=campaign.is_active,
|
|
registrations_count=stats["registrations"],
|
|
total_revenue_kopeks=stats["total_revenue_kopeks"],
|
|
conversion_rate=stats["conversion_rate"],
|
|
created_at=campaign.created_at,
|
|
))
|
|
|
|
return CampaignListResponse(campaigns=items, total=total)
|
|
|
|
|
|
@router.get("/{campaign_id}", response_model=CampaignDetailResponse)
|
|
async def get_campaign(
|
|
campaign_id: int,
|
|
admin: User = Depends(get_current_admin_user),
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
):
|
|
"""Get detailed campaign info."""
|
|
campaign = await get_campaign_by_id(db, campaign_id)
|
|
if not campaign:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Campaign not found",
|
|
)
|
|
|
|
tariff_info = None
|
|
if campaign.tariff:
|
|
tariff_info = TariffInfo(
|
|
id=campaign.tariff.id,
|
|
name=campaign.tariff.name,
|
|
)
|
|
|
|
return CampaignDetailResponse(
|
|
id=campaign.id,
|
|
name=campaign.name,
|
|
start_parameter=campaign.start_parameter,
|
|
bonus_type=campaign.bonus_type,
|
|
is_active=campaign.is_active,
|
|
balance_bonus_kopeks=campaign.balance_bonus_kopeks or 0,
|
|
balance_bonus_rubles=(campaign.balance_bonus_kopeks or 0) / 100,
|
|
subscription_duration_days=campaign.subscription_duration_days,
|
|
subscription_traffic_gb=campaign.subscription_traffic_gb,
|
|
subscription_device_limit=campaign.subscription_device_limit,
|
|
subscription_squads=campaign.subscription_squads or [],
|
|
tariff_id=campaign.tariff_id,
|
|
tariff_duration_days=campaign.tariff_duration_days,
|
|
tariff=tariff_info,
|
|
created_by=campaign.created_by,
|
|
created_at=campaign.created_at,
|
|
updated_at=campaign.updated_at,
|
|
deep_link=_get_deep_link(campaign.start_parameter),
|
|
)
|
|
|
|
|
|
@router.get("/{campaign_id}/stats", response_model=CampaignStatisticsResponse)
|
|
async def get_campaign_stats(
|
|
campaign_id: int,
|
|
admin: User = Depends(get_current_admin_user),
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
):
|
|
"""Get detailed campaign statistics."""
|
|
campaign = await get_campaign_by_id(db, campaign_id)
|
|
if not campaign:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Campaign not found",
|
|
)
|
|
|
|
stats = await get_campaign_statistics(db, campaign_id)
|
|
|
|
return CampaignStatisticsResponse(
|
|
id=campaign.id,
|
|
name=campaign.name,
|
|
start_parameter=campaign.start_parameter,
|
|
bonus_type=campaign.bonus_type,
|
|
is_active=campaign.is_active,
|
|
registrations=stats["registrations"],
|
|
balance_issued_kopeks=stats["balance_issued"],
|
|
balance_issued_rubles=stats["balance_issued"] / 100,
|
|
subscription_issued=stats["subscription_issued"],
|
|
last_registration=stats["last_registration"],
|
|
total_revenue_kopeks=stats["total_revenue_kopeks"],
|
|
total_revenue_rubles=stats["total_revenue_kopeks"] / 100,
|
|
avg_revenue_per_user_kopeks=stats["avg_revenue_per_user_kopeks"],
|
|
avg_revenue_per_user_rubles=stats["avg_revenue_per_user_kopeks"] / 100,
|
|
avg_first_payment_kopeks=stats["avg_first_payment_kopeks"],
|
|
avg_first_payment_rubles=stats["avg_first_payment_kopeks"] / 100,
|
|
trial_users_count=stats["trial_users_count"],
|
|
active_trials_count=stats["active_trials_count"],
|
|
conversion_count=stats["conversion_count"],
|
|
paid_users_count=stats["paid_users_count"],
|
|
conversion_rate=stats["conversion_rate"],
|
|
trial_conversion_rate=stats["trial_conversion_rate"],
|
|
deep_link=_get_deep_link(campaign.start_parameter),
|
|
)
|
|
|
|
|
|
@router.get("/{campaign_id}/registrations", response_model=CampaignRegistrationsResponse)
|
|
async def get_campaign_registrations(
|
|
campaign_id: int,
|
|
page: int = Query(1, ge=1),
|
|
per_page: int = Query(50, ge=1, le=100),
|
|
admin: User = Depends(get_current_admin_user),
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
):
|
|
"""Get list of users registered through campaign."""
|
|
campaign = await get_campaign_by_id(db, campaign_id)
|
|
if not campaign:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Campaign not found",
|
|
)
|
|
|
|
offset = (page - 1) * per_page
|
|
|
|
# Get registrations with user info
|
|
result = await db.execute(
|
|
select(AdvertisingCampaignRegistration, User)
|
|
.join(User, AdvertisingCampaignRegistration.user_id == User.id)
|
|
.where(AdvertisingCampaignRegistration.campaign_id == campaign_id)
|
|
.order_by(AdvertisingCampaignRegistration.created_at.desc())
|
|
.offset(offset)
|
|
.limit(per_page)
|
|
)
|
|
rows = result.all()
|
|
|
|
# Count total
|
|
count_result = await db.execute(
|
|
select(func.count(AdvertisingCampaignRegistration.id))
|
|
.where(AdvertisingCampaignRegistration.campaign_id == campaign_id)
|
|
)
|
|
total = count_result.scalar() or 0
|
|
|
|
items = []
|
|
for reg, user in rows:
|
|
# Check if user has subscription
|
|
sub_result = await db.execute(
|
|
select(Subscription)
|
|
.where(
|
|
Subscription.user_id == user.id,
|
|
Subscription.status == "active",
|
|
)
|
|
.limit(1)
|
|
)
|
|
has_sub = sub_result.scalar_one_or_none() is not None
|
|
|
|
items.append(CampaignRegistrationItem(
|
|
id=reg.id,
|
|
user_id=user.id,
|
|
telegram_id=user.telegram_id,
|
|
username=user.username,
|
|
first_name=user.first_name,
|
|
bonus_type=reg.bonus_type,
|
|
balance_bonus_kopeks=reg.balance_bonus_kopeks or 0,
|
|
subscription_duration_days=reg.subscription_duration_days,
|
|
tariff_id=reg.tariff_id,
|
|
tariff_duration_days=reg.tariff_duration_days,
|
|
created_at=reg.created_at,
|
|
user_balance_kopeks=user.balance_kopeks or 0,
|
|
has_subscription=has_sub,
|
|
has_paid=user.has_had_paid_subscription or False,
|
|
))
|
|
|
|
return CampaignRegistrationsResponse(
|
|
registrations=items,
|
|
total=total,
|
|
page=page,
|
|
per_page=per_page,
|
|
)
|
|
|
|
|
|
@router.post("", response_model=CampaignDetailResponse)
|
|
async def create_new_campaign(
|
|
request: CampaignCreateRequest,
|
|
admin: User = Depends(get_current_admin_user),
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
):
|
|
"""Create a new advertising campaign."""
|
|
# Check if start_parameter is unique
|
|
existing = await get_campaign_by_start_parameter(db, request.start_parameter)
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Campaign with start parameter '{request.start_parameter}' already exists",
|
|
)
|
|
|
|
# Validate tariff exists if tariff bonus type
|
|
if request.bonus_type == "tariff":
|
|
if not request.tariff_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Tariff ID is required for tariff bonus type",
|
|
)
|
|
tariff_result = await db.execute(
|
|
select(Tariff).where(Tariff.id == request.tariff_id)
|
|
)
|
|
tariff = tariff_result.scalar_one_or_none()
|
|
if not tariff:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Tariff not found",
|
|
)
|
|
|
|
campaign = await create_campaign(
|
|
db,
|
|
name=request.name,
|
|
start_parameter=request.start_parameter,
|
|
bonus_type=request.bonus_type,
|
|
created_by=admin.id,
|
|
balance_bonus_kopeks=request.balance_bonus_kopeks,
|
|
subscription_duration_days=request.subscription_duration_days,
|
|
subscription_traffic_gb=request.subscription_traffic_gb,
|
|
subscription_device_limit=request.subscription_device_limit,
|
|
subscription_squads=request.subscription_squads,
|
|
tariff_id=request.tariff_id,
|
|
tariff_duration_days=request.tariff_duration_days,
|
|
is_active=request.is_active,
|
|
)
|
|
|
|
# Reload to get tariff relationship
|
|
campaign = await get_campaign_by_id(db, campaign.id)
|
|
|
|
logger.info(f"Admin {admin.id} created campaign {campaign.id}: {campaign.name}")
|
|
|
|
return await get_campaign(campaign.id, admin, db)
|
|
|
|
|
|
@router.put("/{campaign_id}", response_model=CampaignDetailResponse)
|
|
async def update_existing_campaign(
|
|
campaign_id: int,
|
|
request: CampaignUpdateRequest,
|
|
admin: User = Depends(get_current_admin_user),
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
):
|
|
"""Update an existing campaign."""
|
|
campaign = await get_campaign_by_id(db, campaign_id)
|
|
if not campaign:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Campaign not found",
|
|
)
|
|
|
|
# Check if start_parameter is unique (if changing)
|
|
if request.start_parameter and request.start_parameter != campaign.start_parameter:
|
|
existing = await get_campaign_by_start_parameter(db, request.start_parameter)
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Campaign with start parameter '{request.start_parameter}' already exists",
|
|
)
|
|
|
|
# Validate tariff if changing to tariff bonus type
|
|
if request.bonus_type == "tariff" or (campaign.bonus_type == "tariff" and request.tariff_id):
|
|
tariff_id = request.tariff_id or campaign.tariff_id
|
|
if tariff_id:
|
|
tariff_result = await db.execute(
|
|
select(Tariff).where(Tariff.id == tariff_id)
|
|
)
|
|
tariff = tariff_result.scalar_one_or_none()
|
|
if not tariff:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Tariff not found",
|
|
)
|
|
|
|
# Build updates
|
|
updates = {}
|
|
if request.name is not None:
|
|
updates["name"] = request.name
|
|
if request.start_parameter is not None:
|
|
updates["start_parameter"] = request.start_parameter
|
|
if request.bonus_type is not None:
|
|
updates["bonus_type"] = request.bonus_type
|
|
if request.is_active is not None:
|
|
updates["is_active"] = request.is_active
|
|
if request.balance_bonus_kopeks is not None:
|
|
updates["balance_bonus_kopeks"] = request.balance_bonus_kopeks
|
|
if request.subscription_duration_days is not None:
|
|
updates["subscription_duration_days"] = request.subscription_duration_days
|
|
if request.subscription_traffic_gb is not None:
|
|
updates["subscription_traffic_gb"] = request.subscription_traffic_gb
|
|
if request.subscription_device_limit is not None:
|
|
updates["subscription_device_limit"] = request.subscription_device_limit
|
|
if request.subscription_squads is not None:
|
|
updates["subscription_squads"] = request.subscription_squads
|
|
if request.tariff_id is not None:
|
|
updates["tariff_id"] = request.tariff_id
|
|
if request.tariff_duration_days is not None:
|
|
updates["tariff_duration_days"] = request.tariff_duration_days
|
|
|
|
if updates:
|
|
await update_campaign(db, campaign, **updates)
|
|
|
|
logger.info(f"Admin {admin.id} updated campaign {campaign_id}")
|
|
|
|
return await get_campaign(campaign_id, admin, db)
|
|
|
|
|
|
@router.delete("/{campaign_id}")
|
|
async def delete_existing_campaign(
|
|
campaign_id: int,
|
|
admin: User = Depends(get_current_admin_user),
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
):
|
|
"""Delete a campaign."""
|
|
campaign = await get_campaign_by_id(db, campaign_id)
|
|
if not campaign:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Campaign not found",
|
|
)
|
|
|
|
# Check if campaign has registrations
|
|
reg_count = len(campaign.registrations) if campaign.registrations else 0
|
|
if reg_count > 0:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Cannot delete campaign with {reg_count} registrations. Deactivate it instead.",
|
|
)
|
|
|
|
await delete_campaign(db, campaign)
|
|
logger.info(f"Admin {admin.id} deleted campaign {campaign_id}: {campaign.name}")
|
|
|
|
return {"message": "Campaign deleted successfully"}
|
|
|
|
|
|
@router.post("/{campaign_id}/toggle", response_model=CampaignToggleResponse)
|
|
async def toggle_campaign(
|
|
campaign_id: int,
|
|
admin: User = Depends(get_current_admin_user),
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
):
|
|
"""Toggle campaign active status."""
|
|
campaign = await get_campaign_by_id(db, campaign_id)
|
|
if not campaign:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Campaign not found",
|
|
)
|
|
|
|
new_status = not campaign.is_active
|
|
await update_campaign(db, campaign, is_active=new_status)
|
|
|
|
status_text = "activated" if new_status else "deactivated"
|
|
logger.info(f"Admin {admin.id} {status_text} campaign {campaign_id}")
|
|
|
|
return CampaignToggleResponse(
|
|
id=campaign_id,
|
|
is_active=new_status,
|
|
message=f"Campaign {status_text}",
|
|
)
|