Files
remnawave-bedolaga-telegram…/app/database/crud/subscription_conversion.py
Fringg eb18994b7d fix: complete datetime.utcnow() → datetime.now(UTC) migration
- Migrate 660+ datetime.utcnow() across 153 files to datetime.now(UTC)
- Migrate 30+ datetime.now() without UTC to datetime.now(UTC)
- Convert all 170 DateTime columns to DateTime(timezone=True)
- Add migrate_datetime_to_timestamptz() in universal_migration with SET LOCAL timezone='UTC' safety
- Remove 70+ .replace(tzinfo=None) workarounds
- Fix utcfromtimestamp → fromtimestamp(..., tz=UTC)
- Fix fromtimestamp() without tz= (system_logs, backup_service, referral_diagnostics)
- Fix fromisoformat/isoparse to ensure aware output (platega, yookassa, wata, miniapp, nalogo)
- Fix strptime() to add .replace(tzinfo=UTC) (backup_service, referral_diagnostics)
- Fix datetime.combine() to include tzinfo=UTC (remnawave_sync, traffic_monitoring)
- Fix datetime.max/datetime.min sentinels with .replace(tzinfo=UTC)
- Rename panel_datetime_to_naive_utc → panel_datetime_to_utc
- Remove DTZ003 from ruff ignore list
2026-02-17 04:45:40 +03:00

115 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import UTC, datetime, timedelta
import structlog
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import SubscriptionConversion, User
logger = structlog.get_logger(__name__)
async def create_subscription_conversion(
db: AsyncSession,
user_id: int,
trial_duration_days: int,
payment_method: str,
first_payment_amount_kopeks: int,
first_paid_period_days: int,
) -> SubscriptionConversion:
conversion = SubscriptionConversion(
user_id=user_id,
converted_at=datetime.now(UTC),
trial_duration_days=trial_duration_days,
payment_method=payment_method,
first_payment_amount_kopeks=first_payment_amount_kopeks,
first_paid_period_days=first_paid_period_days,
)
db.add(conversion)
await db.commit()
await db.refresh(conversion)
logger.info(
'✅ Создана запись о конверсии для пользователя дн. → дн. за ₽',
user_id=user_id,
trial_duration_days=trial_duration_days,
first_paid_period_days=first_paid_period_days,
first_payment_amount_kopeks=first_payment_amount_kopeks / 100,
)
return conversion
async def get_conversion_by_user_id(db: AsyncSession, user_id: int) -> SubscriptionConversion | None:
result = await db.execute(
select(SubscriptionConversion)
.where(SubscriptionConversion.user_id == user_id)
.order_by(SubscriptionConversion.converted_at.desc())
.limit(1)
)
return result.scalar_one_or_none()
async def get_conversion_statistics(db: AsyncSession) -> dict:
from app.database.models import Subscription
# Получаем количество записей о конверсиях в таблице
total_conversions_result = await db.execute(select(func.count(SubscriptionConversion.id)))
total_conversions = total_conversions_result.scalar() or 0
# Подсчитываем пользователей с платными подписками
users_with_paid_result = await db.execute(select(func.count(User.id)).where(User.has_had_paid_subscription == True))
users_with_paid = users_with_paid_result.scalar() or 0
# Подсчитываем всех пользователей с подписками (использовавших триал)
# Считаем что все новые пользователи начинают с триала
total_users_with_subscriptions_result = await db.execute(select(func.count(func.distinct(Subscription.user_id))))
total_users_with_subscriptions = total_users_with_subscriptions_result.scalar() or 0
# Расчёт конверсии: (оплатившие) / (всего с подписками) * 100
# Это показывает какой % пользователей, получивших подписку, в итоге оплатили
if total_users_with_subscriptions > 0:
conversion_rate = round((users_with_paid / total_users_with_subscriptions) * 100, 1)
else:
conversion_rate = 0.0
avg_trial_duration_result = await db.execute(select(func.avg(SubscriptionConversion.trial_duration_days)))
avg_trial_duration = avg_trial_duration_result.scalar() or 0
avg_first_payment_result = await db.execute(select(func.avg(SubscriptionConversion.first_payment_amount_kopeks)))
avg_first_payment = avg_first_payment_result.scalar() or 0
month_ago = datetime.now(UTC) - timedelta(days=30)
month_conversions_result = await db.execute(
select(func.count(SubscriptionConversion.id)).where(SubscriptionConversion.converted_at >= month_ago)
)
month_conversions = month_conversions_result.scalar() or 0
logger.info('📊 Статистика конверсий:')
logger.info('Всего пользователей с подписками', total_users_with_subscriptions=total_users_with_subscriptions)
logger.info('Оплативших подписку', users_with_paid=users_with_paid)
logger.info('Рассчитанная конверсия', conversion_rate=conversion_rate)
return {
'total_conversions': total_conversions,
'conversion_rate': conversion_rate,
'users_with_trial': total_users_with_subscriptions,
'converted_users': users_with_paid,
'avg_trial_duration_days': round(avg_trial_duration, 1),
'avg_first_payment_rubles': round((avg_first_payment or 0) / 100, 2),
'month_conversions': month_conversions,
}
async def get_users_had_trial_count(db: AsyncSession) -> int:
conversions_count_result = await db.execute(select(func.count(func.distinct(SubscriptionConversion.user_id))))
conversions_count = conversions_count_result.scalar()
paid_users_result = await db.execute(select(func.count(User.id)).where(User.has_had_paid_subscription == True))
paid_users_count = paid_users_result.scalar()
return max(conversions_count, paid_users_count)