From 97c8ddf4e1ee79dc7fda5a15b49231b0f3e3f2d3 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 23 Sep 2025 02:54:19 +0300 Subject: [PATCH] Add advertising campaign stats to admin user info --- app/database/crud/campaign.py | 93 +++++++++++++++++++++++++++++++++ app/handlers/admin/campaigns.py | 29 ++++++++++ app/handlers/admin/users.py | 81 ++++++++++++++++++++++++++-- 3 files changed, 198 insertions(+), 5 deletions(-) diff --git a/app/database/crud/campaign.py b/app/database/crud/campaign.py index 40313434..b7c9e10d 100644 --- a/app/database/crud/campaign.py +++ b/app/database/crud/campaign.py @@ -9,6 +9,12 @@ from sqlalchemy.orm import selectinload from app.database.models import ( AdvertisingCampaign, AdvertisingCampaignRegistration, + Subscription, + SubscriptionConversion, + SubscriptionStatus, + Transaction, + TransactionType, + User, ) logger = logging.getLogger(__name__) @@ -157,6 +163,19 @@ async def delete_campaign(db: AsyncSession, campaign: AdvertisingCampaign) -> bo return True +async def get_campaign_registration_by_user( + db: AsyncSession, + user_id: int, +) -> Optional[AdvertisingCampaignRegistration]: + result = await db.execute( + select(AdvertisingCampaignRegistration) + .options(selectinload(AdvertisingCampaignRegistration.campaign)) + .where(AdvertisingCampaignRegistration.user_id == user_id) + .limit(1) + ) + return result.scalar_one_or_none() + + async def record_campaign_registration( db: AsyncSession, *, @@ -197,6 +216,11 @@ async def get_campaign_statistics( db: AsyncSession, campaign_id: int, ) -> Dict[str, Optional[int]]: + registrations_query = select(AdvertisingCampaignRegistration.user_id).where( + AdvertisingCampaignRegistration.campaign_id == campaign_id + ) + registrations_subquery = registrations_query.subquery() + result = await db.execute( select( func.count(AdvertisingCampaignRegistration.id), @@ -217,11 +241,80 @@ async def get_campaign_statistics( ) ) + deposits_result = await db.execute( + select(func.coalesce(func.sum(Transaction.amount_kopeks), 0)).where( + Transaction.user_id.in_(select(registrations_subquery.c.user_id)), + Transaction.type == TransactionType.DEPOSIT.value, + Transaction.is_completed.is_(True), + ) + ) + total_revenue = deposits_result.scalar() or 0 + + trials_result = await db.execute( + select(func.count(func.distinct(Subscription.user_id))).where( + Subscription.user_id.in_(select(registrations_subquery.c.user_id)), + Subscription.is_trial.is_(True), + ) + ) + trial_users_count = trials_result.scalar() or 0 + + active_trials_result = await db.execute( + select(func.count(func.distinct(Subscription.user_id))).where( + Subscription.user_id.in_(select(registrations_subquery.c.user_id)), + Subscription.is_trial.is_(True), + Subscription.status == SubscriptionStatus.ACTIVE.value, + ) + ) + active_trials_count = active_trials_result.scalar() or 0 + + conversions_result = await db.execute( + select(func.count(func.distinct(SubscriptionConversion.user_id))).where( + SubscriptionConversion.user_id.in_(select(registrations_subquery.c.user_id)) + ) + ) + conversion_count = conversions_result.scalar() or 0 + + paid_users_result = await db.execute( + select(func.count(User.id)).where( + User.id.in_(select(registrations_subquery.c.user_id)), + User.has_had_paid_subscription.is_(True), + ) + ) + paid_users_count = paid_users_result.scalar() or 0 + + avg_first_payment_result = await db.execute( + select(func.coalesce(func.avg(SubscriptionConversion.first_payment_amount_kopeks), 0)).where( + SubscriptionConversion.user_id.in_(select(registrations_subquery.c.user_id)) + ) + ) + avg_first_payment = int(avg_first_payment_result.scalar() or 0) + + conversion_rate = 0.0 + if count: + conversion_rate = round((paid_users_count / count) * 100, 1) + + trial_conversion_rate = 0.0 + if trial_users_count: + trial_conversion_rate = round((conversion_count / trial_users_count) * 100, 1) + + avg_revenue_per_user = 0 + if count: + avg_revenue_per_user = int(total_revenue / count) + return { "registrations": count or 0, "balance_issued": total_balance or 0, "subscription_issued": subscription_count_result.scalar() or 0, "last_registration": last_registration, + "total_revenue_kopeks": total_revenue, + "trial_users_count": trial_users_count, + "active_trials_count": active_trials_count, + "conversion_count": conversion_count, + "paid_users_count": paid_users_count, + "conversion_rate": conversion_rate, + "trial_conversion_rate": trial_conversion_rate, + "avg_revenue_per_user_kopeks": avg_revenue_per_user, + "avg_first_payment_kopeks": avg_first_payment, } diff --git a/app/handlers/admin/campaigns.py b/app/handlers/admin/campaigns.py index 22ff8aac..f6b9b325 100644 --- a/app/handlers/admin/campaigns.py +++ b/app/handlers/admin/campaigns.py @@ -333,6 +333,35 @@ async def show_campaign_detail( f"• Выдано баланса: {texts.format_price(stats['balance_issued'])}" ) text.append(f"• Выдано подписок: {stats['subscription_issued']}") + text.append( + f"• Доход: {texts.format_price(stats['total_revenue_kopeks'])}" + ) + text.append( + "• Получили триал: " + f"{stats['trial_users_count']}" + f" (активно: {stats['active_trials_count']})" + ) + text.append( + "• Конверсий в оплату: " + f"{stats['conversion_count']}" + f" / пользователей с оплатой: {stats['paid_users_count']}" + ) + text.append( + "• Конверсия в оплату: " + f"{stats['conversion_rate']:.1f}%" + ) + text.append( + "• Конверсия триала: " + f"{stats['trial_conversion_rate']:.1f}%" + ) + text.append( + "• Средний доход на пользователя: " + f"{texts.format_price(stats['avg_revenue_per_user_kopeks'])}" + ) + text.append( + "• Средний первый платеж: " + f"{texts.format_price(stats['avg_first_payment_kopeks'])}" + ) if stats["last_registration"]: text.append( f"• Последняя: {stats['last_registration'].strftime('%d.%m.%Y %H:%M')}" diff --git a/app/handlers/admin/users.py b/app/handlers/admin/users.py index 620e4001..45fe983f 100644 --- a/app/handlers/admin/users.py +++ b/app/handlers/admin/users.py @@ -8,7 +8,11 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.states import AdminStates from app.database.models import User, UserStatus, Subscription, SubscriptionStatus, TransactionType -from app.database.crud.user import get_user_by_id +from app.database.crud.user import get_user_by_id +from app.database.crud.campaign import ( + get_campaign_registration_by_user, + get_campaign_statistics, +) from app.keyboards.admin import ( get_admin_users_keyboard, get_user_management_keyboard, get_admin_pagination_keyboard, get_confirmation_keyboard, @@ -1214,6 +1218,10 @@ async def show_user_statistics( subscription = profile["subscription"] referral_stats = await get_detailed_referral_stats(db, user.id) + campaign_registration = await get_campaign_registration_by_user(db, user.id) + campaign_stats = None + if campaign_registration: + campaign_stats = await get_campaign_statistics(db, campaign_registration.campaign_id) text = f"📊 Статистика пользователя\n\n" text += f"👤 {user.full_name} (ID: {user.telegram_id})\n\n" @@ -1236,17 +1244,80 @@ async def show_user_statistics( text += f"• Отсутствует\n" text += f"\nРеферальная программа:\n" - + if user.referred_by_id: referrer = await get_user_by_id(db, user.referred_by_id) if referrer: text += f"• Пришел по реферальной ссылке от {referrer.full_name}\n" else: - text += f"• Пришел по реферальной ссылке (реферер не найден)\n" + text += "• Пришел по реферальной ссылке (реферер не найден)\n" + if campaign_registration and campaign_registration.campaign: + text += ( + "• Дополнительно зарегистрирован через кампанию " + f"{campaign_registration.campaign.name}\n" + ) + elif campaign_registration and campaign_registration.campaign: + text += ( + "• Регистрация через рекламную кампанию " + f"{campaign_registration.campaign.name}\n" + ) + if campaign_registration.created_at: + text += ( + "• Дата регистрации по кампании: " + f"{campaign_registration.created_at.strftime('%d.%m.%Y %H:%M')}\n" + ) else: - text += f"• Прямая регистрация\n" - + text += "• Прямая регистрация\n" + text += f"• Реферальный код: {user.referral_code}\n\n" + + if campaign_registration and campaign_registration.campaign and campaign_stats: + text += "Рекламная кампания:\n" + text += ( + "• Название: " + f"{campaign_registration.campaign.name}" + ) + if campaign_registration.campaign.start_parameter: + text += ( + " (параметр: " + f"{campaign_registration.campaign.start_parameter})" + ) + text += "\n" + text += ( + "• Всего регистраций: " + f"{campaign_stats['registrations']}\n" + ) + text += ( + "• Суммарный доход: " + f"{settings.format_price(campaign_stats['total_revenue_kopeks'])}\n" + ) + text += ( + "• Получили триал: " + f"{campaign_stats['trial_users_count']}" + f" (активно: {campaign_stats['active_trials_count']})\n" + ) + text += ( + "• Конверсий в оплату: " + f"{campaign_stats['conversion_count']}" + f" (оплативших пользователей: {campaign_stats['paid_users_count']})\n" + ) + text += ( + "• Конверсия в оплату: " + f"{campaign_stats['conversion_rate']:.1f}%\n" + ) + text += ( + "• Конверсия триала: " + f"{campaign_stats['trial_conversion_rate']:.1f}%\n" + ) + text += ( + "• Средний доход на пользователя: " + f"{settings.format_price(campaign_stats['avg_revenue_per_user_kopeks'])}\n" + ) + text += ( + "• Средний первый платеж: " + f"{settings.format_price(campaign_stats['avg_first_payment_kopeks'])}\n" + ) + text += "\n" if referral_stats['invited_count'] > 0: text += f"Доходы от рефералов:\n"