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"