feat: split subscription stats by access type
This commit is contained in:
@@ -71,11 +71,17 @@ async def show_statistics_handler(
|
||||
f"📊 {_('admin_user_stats_total_label')}: <b>{user_stats['total_users']}</b>"
|
||||
)
|
||||
# Removed: Active today moved to panel stats
|
||||
stats_text_parts.append(
|
||||
f"📡 {_('admin_user_stats_active_subscription_label')}: <b>{user_stats['active_subscriptions']}</b>" # noqa: E501
|
||||
)
|
||||
stats_text_parts.append(
|
||||
f"💳 {_('admin_user_stats_paid_subs_label')}: <b>{user_stats['paid_subscriptions']}</b>"
|
||||
)
|
||||
stats_text_parts.append(
|
||||
f"🆓 {_('admin_user_stats_trial_label')}: <b>{user_stats['trial_users']}</b>"
|
||||
f"🧪 {_('admin_user_stats_trial_label')}: <b>{user_stats['trial_users']}</b>"
|
||||
)
|
||||
stats_text_parts.append(
|
||||
f"🎁 {_('admin_user_stats_free_subscription_label')}: <b>{user_stats['free_subscription_users']}</b>" # noqa: E501
|
||||
)
|
||||
stats_text_parts.append(
|
||||
f"😴 {_('admin_user_stats_inactive_label')}: <b>{user_stats['inactive_users']}</b>"
|
||||
|
||||
@@ -166,8 +166,10 @@ async def create_user_stats_result(
|
||||
"inline_user_stats_message",
|
||||
total=user_stats["total_users"],
|
||||
active_today=user_stats["active_today"],
|
||||
active=user_stats["active_subscriptions"],
|
||||
paid=user_stats["paid_subscriptions"],
|
||||
trial=user_stats["trial_users"],
|
||||
free=user_stats["free_subscription_users"],
|
||||
inactive=user_stats["inactive_users"],
|
||||
banned=user_stats["banned_users"],
|
||||
referral=user_stats["referral_users"],
|
||||
@@ -179,7 +181,7 @@ async def create_user_stats_result(
|
||||
description=_(
|
||||
"inline_user_stats_description",
|
||||
total=user_stats["total_users"],
|
||||
active=user_stats["paid_subscriptions"],
|
||||
active=user_stats["active_subscriptions"],
|
||||
),
|
||||
input_message_content=InputTextMessageContent(
|
||||
message_text=stats_text, parse_mode="HTML"
|
||||
|
||||
+67
-14
@@ -666,17 +666,33 @@ async def get_enhanced_user_statistics(session: AsyncSession) -> Dict[str, Any]:
|
||||
active_today = int(user_counts[2] or 0)
|
||||
referral_users = int(user_counts[3] or 0)
|
||||
|
||||
subscription_counts_stmt = (
|
||||
provider_value = func.lower(func.coalesce(Subscription.provider, ""))
|
||||
panel_status_value = func.upper(func.coalesce(Subscription.status_from_panel, ""))
|
||||
trial_subscription_condition = or_(
|
||||
provider_value == "trial",
|
||||
panel_status_value == "TRIAL",
|
||||
)
|
||||
paid_subscription_condition = and_(
|
||||
provider_value != "",
|
||||
provider_value != "trial",
|
||||
panel_status_value != "TRIAL",
|
||||
)
|
||||
free_subscription_condition = and_(
|
||||
provider_value == "",
|
||||
panel_status_value != "TRIAL",
|
||||
)
|
||||
|
||||
active_subscription_flags_sq = (
|
||||
select(
|
||||
func.count(
|
||||
func.distinct(
|
||||
case((Subscription.provider.is_not(None), Subscription.user_id), else_=None)
|
||||
)
|
||||
Subscription.user_id.label("user_id"),
|
||||
func.max(case((paid_subscription_condition, 1), else_=0)).label(
|
||||
"has_paid_subscription"
|
||||
),
|
||||
func.count(
|
||||
func.distinct(
|
||||
case((Subscription.provider.is_(None), Subscription.user_id), else_=None)
|
||||
)
|
||||
func.max(case((trial_subscription_condition, 1), else_=0)).label(
|
||||
"has_trial_subscription"
|
||||
),
|
||||
func.max(case((free_subscription_condition, 1), else_=0)).label(
|
||||
"has_free_subscription"
|
||||
),
|
||||
)
|
||||
.join(User, Subscription.user_id == User.user_id)
|
||||
@@ -686,27 +702,64 @@ async def get_enhanced_user_statistics(session: AsyncSession) -> Dict[str, Any]:
|
||||
Subscription.end_date > now,
|
||||
)
|
||||
)
|
||||
.group_by(Subscription.user_id)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
subscription_counts_stmt = select(
|
||||
func.count(active_subscription_flags_sq.c.user_id),
|
||||
func.coalesce(func.sum(active_subscription_flags_sq.c.has_paid_subscription), 0),
|
||||
func.coalesce(
|
||||
func.sum(
|
||||
case(
|
||||
(
|
||||
active_subscription_flags_sq.c.has_paid_subscription == 0,
|
||||
active_subscription_flags_sq.c.has_trial_subscription,
|
||||
),
|
||||
else_=0,
|
||||
)
|
||||
),
|
||||
0,
|
||||
),
|
||||
func.coalesce(
|
||||
func.sum(
|
||||
case(
|
||||
(
|
||||
and_(
|
||||
active_subscription_flags_sq.c.has_paid_subscription == 0,
|
||||
active_subscription_flags_sq.c.has_trial_subscription == 0,
|
||||
),
|
||||
active_subscription_flags_sq.c.has_free_subscription,
|
||||
),
|
||||
else_=0,
|
||||
)
|
||||
),
|
||||
0,
|
||||
),
|
||||
)
|
||||
subscription_counts = (await session.execute(subscription_counts_stmt)).one()
|
||||
paid_subs_users = int(subscription_counts[0] or 0)
|
||||
trial_users = int(subscription_counts[1] or 0)
|
||||
active_subscription_users = int(subscription_counts[0] or 0)
|
||||
paid_subs_users = int(subscription_counts[1] or 0)
|
||||
trial_users = int(subscription_counts[2] or 0)
|
||||
free_subscription_users = int(subscription_counts[3] or 0)
|
||||
|
||||
# Inactive users (no active subscription)
|
||||
inactive_users = total_users - paid_subs_users - trial_users - banned_users
|
||||
inactive_users = total_users - active_subscription_users
|
||||
|
||||
return {
|
||||
"total_users": total_users,
|
||||
"banned_users": banned_users,
|
||||
"active_today": active_today,
|
||||
"active_subscriptions": active_subscription_users,
|
||||
"paid_subscriptions": paid_subs_users,
|
||||
"trial_users": trial_users,
|
||||
"free_subscription_users": free_subscription_users,
|
||||
"inactive_users": max(0, inactive_users),
|
||||
"referral_users": referral_users,
|
||||
}
|
||||
|
||||
|
||||
async def get_user_ids_with_active_subscription(session: AsyncSession) -> List[int]:
|
||||
"""Return non-banned user IDs who have an active subscription (paid or trial)."""
|
||||
"""Return non-banned user IDs who have any active subscription."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
@@ -492,7 +492,7 @@
|
||||
<AdminDashboardStack>
|
||||
<AdminSectionHeader title={at("stats_section_audience", {}, "")} />
|
||||
<AdminDashboardGrid columns={3}>
|
||||
{#each Array(3) as _, i (i)}
|
||||
{#each Array(6) as _, i (i)}
|
||||
<Card.Root class="admin-cn-card-skeleton">
|
||||
<Card.Header>
|
||||
<span class="admin-skeleton admin-skeleton-line admin-skeleton-line-short"></span>
|
||||
@@ -690,22 +690,96 @@
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Description>{at("stats_label_active_subs", {}, "")}</Card.Description>
|
||||
<Card.Title>{users.active_subscriptions ?? 0}</Card.Title>
|
||||
<Card.Action>
|
||||
<Badge variant="outline"
|
||||
>{users.total_users
|
||||
? Math.round(((users.active_subscriptions ?? 0) / (users.total_users || 1)) * 100)
|
||||
: 0}%</Badge
|
||||
>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
<Card.Footer class="admin-cn-card-footer--stack">
|
||||
<div class="admin-cn-card-footer-primary">
|
||||
{at("stats_trend_paid", { count: users.paid_subscriptions ?? 0 }, "")}
|
||||
</div>
|
||||
<div class="admin-cn-card-footer-muted">
|
||||
{at("stats_card_active_subs_caption", {}, "")}
|
||||
</div>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Description>{at("stats_label_paid_subs", {}, "")}</Card.Description>
|
||||
<Card.Title>{users.paid_subscriptions ?? 0}</Card.Title>
|
||||
<Card.Action>
|
||||
<Badge variant="outline">{users.trial_users ?? 0}</Badge>
|
||||
<Badge variant="outline"
|
||||
>{users.active_subscriptions
|
||||
? Math.round(
|
||||
((users.paid_subscriptions ?? 0) / (users.active_subscriptions || 1)) * 100
|
||||
)
|
||||
: 0}%</Badge
|
||||
>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
<Card.Footer class="admin-cn-card-footer--stack">
|
||||
<div class="admin-cn-card-footer-primary">
|
||||
{at("stats_trend_trials", { count: users.trial_users ?? 0 }, "")}
|
||||
</div>
|
||||
<div class="admin-cn-card-footer-muted">
|
||||
{at("stats_trend_free", { count: users.free_subscription_users ?? 0 }, "")}
|
||||
</div>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Description>{at("stats_label_trial_users", {}, "")}</Card.Description>
|
||||
<Card.Title>{users.trial_users ?? 0}</Card.Title>
|
||||
<Card.Action>
|
||||
<Badge variant="outline"
|
||||
>{users.active_subscriptions
|
||||
? Math.round(((users.trial_users ?? 0) / (users.active_subscriptions || 1)) * 100)
|
||||
: 0}%</Badge
|
||||
>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
<Card.Footer class="admin-cn-card-footer--stack">
|
||||
<div class="admin-cn-card-footer-primary">
|
||||
{at("stats_card_trial_caption", {}, "")}
|
||||
</div>
|
||||
<div class="admin-cn-card-footer-muted">{at("stats_card_paid_caption", {}, "")}</div>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Description>{at("stats_label_free_users", {}, "")}</Card.Description>
|
||||
<Card.Title>{users.free_subscription_users ?? 0}</Card.Title>
|
||||
<Card.Action>
|
||||
<Badge variant="outline"
|
||||
>{users.active_subscriptions
|
||||
? Math.round(
|
||||
((users.free_subscription_users ?? 0) / (users.active_subscriptions || 1)) * 100
|
||||
)
|
||||
: 0}%</Badge
|
||||
>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
<Card.Footer class="admin-cn-card-footer--stack">
|
||||
<div class="admin-cn-card-footer-primary">
|
||||
{at("stats_card_free_caption", {}, "")}
|
||||
</div>
|
||||
<div class="admin-cn-card-footer-muted">
|
||||
{at("stats_trend_trials", { count: users.trial_users ?? 0 }, "")}
|
||||
</div>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Description>{at("stats_label_inactive", {}, "")}</Card.Description>
|
||||
|
||||
@@ -1231,7 +1231,17 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
return {
|
||||
ok: true,
|
||||
currency_symbol: "RUB",
|
||||
users: { total_users: 248, active_subscriptions: 172, banned_users: 3 },
|
||||
users: {
|
||||
total_users: 248,
|
||||
active_today: 9,
|
||||
active_subscriptions: 172,
|
||||
paid_subscriptions: 141,
|
||||
trial_users: 8,
|
||||
free_subscription_users: 23,
|
||||
inactive_users: 76,
|
||||
banned_users: 3,
|
||||
referral_users: 34,
|
||||
},
|
||||
financial: {
|
||||
today_revenue: 1240,
|
||||
week_revenue: 15800,
|
||||
|
||||
+16
-6
@@ -347,8 +347,8 @@
|
||||
"admin_user_no_logs": "📜 User has no actions",
|
||||
"admin_user_logs_error": "❌ Error loading user actions",
|
||||
"admin_direct_message_signature": "\n\n---\n💬 Message from administrator",
|
||||
"inline_user_stats_message": "👥 <b>User Statistics</b>\n\n📊 Total: <b>{total}</b>\n📈 Active today: <b>{active_today}</b>\n💳 With paid subscription: <b>{paid}</b>\n🆓 On trial period: <b>{trial}</b>\n😴 Inactive: <b>{inactive}</b>\n🚫 Banned: <b>{banned}</b>\n🎁 Via referral program: <b>{referral}</b>",
|
||||
"inline_user_stats_description": "Total: {total}, Paid: {active}",
|
||||
"inline_user_stats_message": "👥 <b>User Statistics</b>\n\n📊 Total: <b>{total}</b>\n📈 Active today: <b>{active_today}</b>\n📡 With active subscription: <b>{active}</b>\n💳 With paid subscription: <b>{paid}</b>\n🧪 With trial subscription: <b>{trial}</b>\n🎁 With free subscription: <b>{free}</b>\n😴 Without active subscription: <b>{inactive}</b>\n🚫 Banned: <b>{banned}</b>\n🎁 Via referral program: <b>{referral}</b>",
|
||||
"inline_user_stats_description": "Total: {total}, subscribed: {active}",
|
||||
"inline_admin_user_stats_title": "👥 User Statistics",
|
||||
"inline_financial_stats_message": "💰 <b>Financial Statistics</b>\n\n📅 Today: <b>{today:.2f} RUB</b>\n ({today_count} payments)\n📅 Week: <b>{week:.2f} RUB</b>\n📅 Month: <b>{month:.2f} RUB</b>\n🏆 All time: <b>{all_time:.2f} RUB</b>",
|
||||
"inline_admin_financial_stats_title": "💰 Financial Statistics",
|
||||
@@ -488,9 +488,11 @@
|
||||
"inline_referral_description": "Share referral link to get bonuses",
|
||||
"inline_financial_description": "Today: {today} RUB",
|
||||
"inline_system_description": "🟢 Online: {online}, 📊 Active: {active}",
|
||||
"admin_user_stats_active_subscription_label": "With active subscription",
|
||||
"admin_user_stats_paid_subs_label": "With paid subscription",
|
||||
"admin_user_stats_trial_label": "On trial period",
|
||||
"admin_user_stats_inactive_label": "Inactive",
|
||||
"admin_user_stats_trial_label": "With trial subscription",
|
||||
"admin_user_stats_free_subscription_label": "With free subscription",
|
||||
"admin_user_stats_inactive_label": "Without active subscription",
|
||||
"admin_user_stats_banned_label": "Banned",
|
||||
"admin_user_stats_referral_label": "Attracted via referral program",
|
||||
"admin_financial_today_label": "Today",
|
||||
@@ -1098,8 +1100,13 @@
|
||||
"admin_stats_error": "Failed to load statistics: {error}",
|
||||
"admin_stats_label_users": "Users",
|
||||
"admin_stats_trend_banned": "Banned: {count}",
|
||||
"admin_stats_label_paid_subs": "Paid Subscriptions",
|
||||
"admin_stats_label_active_subs": "With active subscription",
|
||||
"admin_stats_label_paid_subs": "Paid users",
|
||||
"admin_stats_label_trial_users": "With trial subscription",
|
||||
"admin_stats_label_free_users": "With free subscription",
|
||||
"admin_stats_trend_paid": "Paid: {count}",
|
||||
"admin_stats_trend_trials": "Trials: {count}",
|
||||
"admin_stats_trend_free": "Free: {count}",
|
||||
"admin_stats_label_today_rev": "Today's Revenue",
|
||||
"admin_stats_trend_payments": "{count} payments",
|
||||
"admin_stats_label_week": "This Week",
|
||||
@@ -1145,7 +1152,10 @@
|
||||
"admin_stats_revenue_avg_check": "Average ticket today: {value}",
|
||||
"admin_stats_revenue_avg_none": "No successful payments today",
|
||||
"admin_stats_revenue_avg_ticket_label": "Avg. ticket (today)",
|
||||
"admin_stats_card_paid_caption": "Trials shown in the badge",
|
||||
"admin_stats_card_active_subs_caption": "Paid, trial, and free access combined",
|
||||
"admin_stats_card_paid_caption": "Paid users separated from trial and free access",
|
||||
"admin_stats_card_trial_caption": "Only real trial subscriptions",
|
||||
"admin_stats_card_free_caption": "Manual extensions and bonus access",
|
||||
"admin_stats_card_inactive_caption": "Share of all users is in the badge",
|
||||
"admin_stats_revenue_last_7_calendar": "Last 7 calendar days total",
|
||||
"admin_stats_revenue_prev_7_calendar": "Previous 7 days",
|
||||
|
||||
+17
-7
@@ -347,8 +347,8 @@
|
||||
"admin_user_no_logs": "📜 У пользователя нет действий",
|
||||
"admin_user_logs_error": "❌ Ошибка загрузки действий пользователя",
|
||||
"admin_direct_message_signature": "\n\n---\n💬 Сообщение от администратора",
|
||||
"inline_user_stats_message": "📊 <b>Статистика Бота</b>\n👥 Пользователи\n\n📊 Всего: <b>{total}</b>\n📈 Активных сегодня: <b>{active_today}</b>\n💳 С платной подпиской: <b>{paid}</b>\n🆓 На пробном периоде: <b>{trial}</b>\n😴 Неактивных: <b>{inactive}</b>\n🚫 Заблокированных: <b>{banned}</b>\n🎁 Привлечено по реферальной программе: <b>{referral}</b>",
|
||||
"inline_user_stats_description": "Всего: {total}, Платных: {active}",
|
||||
"inline_user_stats_message": "📊 <b>Статистика Бота</b>\n👥 Пользователи\n\n📊 Всего: <b>{total}</b>\n📈 Активных сегодня: <b>{active_today}</b>\n📡 С активной подпиской: <b>{active}</b>\n💳 С платной подпиской: <b>{paid}</b>\n🧪 С пробной подпиской: <b>{trial}</b>\n🎁 С бесплатной подпиской: <b>{free}</b>\n😴 Без активной подписки: <b>{inactive}</b>\n🚫 Заблокированных: <b>{banned}</b>\n🎁 Привлечено по реферальной программе: <b>{referral}</b>",
|
||||
"inline_user_stats_description": "Всего: {total}, с подпиской: {active}",
|
||||
"inline_admin_user_stats_title": "👥 Статистика пользователей",
|
||||
"inline_financial_stats_message": "💰 <b>Финансовая статистика</b>\n\n📅 За сегодня: <b>{today:.2f} RUB</b>\n ({today_count} платежей)\n📅 За неделю: <b>{week:.2f} RUB</b>\n📅 За месяц: <b>{month:.2f} RUB</b>\n🏆 За все время: <b>{all_time:.2f} RUB</b>",
|
||||
"inline_admin_financial_stats_title": "💰 Финансовая статистика",
|
||||
@@ -488,9 +488,11 @@
|
||||
"inline_referral_description": "Поделиться реферальной ссылкой для получения бонусов",
|
||||
"inline_financial_description": "Сегодня: {today} RUB",
|
||||
"inline_system_description": "🟢 Онлайн: {online}, 📊 Активных: {active}",
|
||||
"admin_user_stats_active_subscription_label": "С активной подпиской",
|
||||
"admin_user_stats_paid_subs_label": "С платной подпиской",
|
||||
"admin_user_stats_trial_label": "На пробном периоде",
|
||||
"admin_user_stats_inactive_label": "Неактивных",
|
||||
"admin_user_stats_trial_label": "С пробной подпиской",
|
||||
"admin_user_stats_free_subscription_label": "С бесплатной подпиской",
|
||||
"admin_user_stats_inactive_label": "Без активной подписки",
|
||||
"admin_user_stats_banned_label": "Заблокированных",
|
||||
"admin_user_stats_referral_label": "Привлечено по реферальной программе",
|
||||
"admin_financial_today_label": "За сегодня",
|
||||
@@ -1098,8 +1100,13 @@
|
||||
"admin_stats_error": "Не удалось загрузить статистику: {error}",
|
||||
"admin_stats_label_users": "Пользователи",
|
||||
"admin_stats_trend_banned": "В бане: {count}",
|
||||
"admin_stats_label_paid_subs": "Платные подписки",
|
||||
"admin_stats_trend_trials": "Триалы: {count}",
|
||||
"admin_stats_label_active_subs": "С активной подпиской",
|
||||
"admin_stats_label_paid_subs": "Платные пользователи",
|
||||
"admin_stats_label_trial_users": "С пробной подпиской",
|
||||
"admin_stats_label_free_users": "С бесплатной подпиской",
|
||||
"admin_stats_trend_paid": "Платные: {count}",
|
||||
"admin_stats_trend_trials": "Пробные: {count}",
|
||||
"admin_stats_trend_free": "Бесплатные: {count}",
|
||||
"admin_stats_label_today_rev": "Доход за день",
|
||||
"admin_stats_trend_payments": "{count} платежей",
|
||||
"admin_stats_label_week": "За неделю",
|
||||
@@ -1145,7 +1152,10 @@
|
||||
"admin_stats_revenue_avg_check": "Средний чек сегодня: {value}",
|
||||
"admin_stats_revenue_avg_none": "Сегодня без успешных платежей",
|
||||
"admin_stats_revenue_avg_ticket_label": "Средний чек (сегодня)",
|
||||
"admin_stats_card_paid_caption": "Триалы — отдельно в бейдже",
|
||||
"admin_stats_card_active_subs_caption": "Платные, пробные и бесплатные вместе",
|
||||
"admin_stats_card_paid_caption": "Платные отдельно от пробных и бесплатных",
|
||||
"admin_stats_card_trial_caption": "Только реальные пробные подписки",
|
||||
"admin_stats_card_free_caption": "Ручные начисления и бонусный доступ",
|
||||
"admin_stats_card_inactive_caption": "Доля от всех пользователей — в бейдже",
|
||||
"admin_stats_revenue_last_7_calendar": "Сумма за последние 7 дн. (календарь)",
|
||||
"admin_stats_revenue_prev_7_calendar": "Предыдущие 7 дн.",
|
||||
|
||||
@@ -96,9 +96,11 @@ class AdminDbStatsCacheTests(unittest.IsolatedAsyncioTestCase):
|
||||
"total_users": 10,
|
||||
"banned_users": 1,
|
||||
"active_today": 2,
|
||||
"active_subscriptions": 8,
|
||||
"paid_subscriptions": 7,
|
||||
"trial_users": 1,
|
||||
"inactive_users": 1,
|
||||
"free_subscription_users": 0,
|
||||
"inactive_users": 2,
|
||||
"referral_users": 3,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -27,6 +27,49 @@ class FakeResult:
|
||||
return self._scalar_value
|
||||
return [self._scalar_value]
|
||||
|
||||
def one(self):
|
||||
return self._scalar_value
|
||||
|
||||
|
||||
class UserDalStatisticsTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_get_enhanced_user_statistics_splits_paid_trial_and_free_users(self):
|
||||
session = SimpleNamespace(
|
||||
execute=AsyncMock(
|
||||
side_effect=[
|
||||
FakeResult((10, 1, 2, 3)),
|
||||
FakeResult((8, 4, 2, 2)),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
stats = await user_dal.get_enhanced_user_statistics(session)
|
||||
|
||||
self.assertEqual(
|
||||
stats,
|
||||
{
|
||||
"total_users": 10,
|
||||
"banned_users": 1,
|
||||
"active_today": 2,
|
||||
"active_subscriptions": 8,
|
||||
"paid_subscriptions": 4,
|
||||
"trial_users": 2,
|
||||
"free_subscription_users": 2,
|
||||
"inactive_users": 2,
|
||||
"referral_users": 3,
|
||||
},
|
||||
)
|
||||
|
||||
stmt = session.execute.await_args_list[1].args[0]
|
||||
sql = str(
|
||||
stmt.compile(
|
||||
dialect=postgresql.dialect(),
|
||||
compile_kwargs={"literal_binds": True},
|
||||
)
|
||||
).upper()
|
||||
self.assertIn("GROUP BY SUBSCRIPTIONS.USER_ID", sql)
|
||||
self.assertIn("SUBSCRIPTIONS.PROVIDER", sql)
|
||||
self.assertIn("TRIAL", sql)
|
||||
|
||||
|
||||
class UserDalMergeTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_get_panel_user_uuids_for_user_includes_subscription_fallbacks_once(self):
|
||||
|
||||
Reference in New Issue
Block a user