diff --git a/backend/app/routes/admin.py b/backend/app/routes/admin.py index 99dcc1dc..ebdd66df 100644 --- a/backend/app/routes/admin.py +++ b/backend/app/routes/admin.py @@ -589,17 +589,17 @@ async def get_analytics(): {'created_at': period_filter} if period_filter else {} ) - # Focus group runs in period + # Focus group runs: count actual credit deductions per run (most accurate for billing) fg_match = {'type': 'debit', 'description': {'$regex': 'Focus group'}} if period_filter: fg_match['ts'] = period_filter run_count_agg = await db.credit_transactions.count_documents(fg_match) - # Persona creations in period - persona_match = {'type': 'debit', 'description': {'$regex': 'persona'}} + # Persona count: count directly from personas collection (1 doc = 1 persona) + persona_match: dict = {} if period_filter: - persona_match['ts'] = period_filter - persona_count = await db.credit_transactions.count_documents(persona_match) + persona_match['created_at'] = period_filter + persona_count = await db.personas.count_documents(persona_match) # Revenue: credits purchased (credit transactions of type 'purchase') rev_match: dict = {'type': 'purchase'} @@ -607,9 +607,15 @@ async def get_analytics(): rev_match['ts'] = period_filter rev_agg = await db.credit_transactions.aggregate([ {'$match': rev_match}, - {'$group': {'_id': None, 'total_credits': {'$sum': '$amount'}, 'count': {'$sum': 1}}}, + {'$group': { + '_id': None, + 'total_credits': {'$sum': '$amount'}, + 'total_usd': {'$sum': {'$ifNull': ['$amount_usd', 0]}}, + 'count': {'$sum': 1}, + }}, ]).to_list(1) revenue_credits = rev_agg[0]['total_credits'] if rev_agg else 0 + revenue_usd = round(rev_agg[0]['total_usd'], 2) if rev_agg else 0.0 purchase_count = rev_agg[0]['count'] if rev_agg else 0 # Cost (USD) from usage_events @@ -653,6 +659,7 @@ async def get_analytics(): }, 'revenue': { 'credits_sold': revenue_credits, + 'revenue_usd': revenue_usd, 'purchase_count': purchase_count, 'cost_usd': round(total_cost_usd, 4), }, diff --git a/backend/app/routes/billing.py b/backend/app/routes/billing.py index 1a52124f..f3fcf1da 100644 --- a/backend/app/routes/billing.py +++ b/backend/app/routes/billing.py @@ -164,12 +164,15 @@ async def stripe_webhook(): # Atomic idempotency: insert the ledger slot first so concurrent Stripe # retries both see the record — only the request that inserts it proceeds + amount_usd = round((session.amount_total or 0) / 100, 2) # cents → dollars + slot = await db.credit_transactions.update_one( {"ref.stripe_payment_id": payment_id}, {"$setOnInsert": { "user_id": user_id, "type": "purchase", "amount": credits, + "amount_usd": amount_usd, "balance_after": None, "description": f"Purchased {pack_id} pack ({credits} credits)", "ref": {"stripe_payment_id": payment_id}, diff --git a/src/components/admin/AnalyticsTab.tsx b/src/components/admin/AnalyticsTab.tsx index ebbb8c3b..738ec5cf 100644 --- a/src/components/admin/AnalyticsTab.tsx +++ b/src/components/admin/AnalyticsTab.tsx @@ -7,7 +7,7 @@ import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recha interface Analytics { users: { total: number; new_in_period: number }; activity: { focus_group_runs: number; personas_created: number }; - revenue: { credits_sold: number; purchase_count: number; cost_usd: number }; + revenue: { credits_sold: number; revenue_usd: number; purchase_count: number; cost_usd: number }; model_breakdown: Array<{ _id: string; cost: number; calls: number }>; daily_purchases: Array<{ _id: string; credits: number; count: number }>; } @@ -41,7 +41,7 @@ export default function AnalyticsTab() { if (loading) return
; if (!data) return

Failed to load analytics.

; - const revenueUsd = data.revenue.credits_sold; + const revenueUsd = data.revenue.revenue_usd; const costUsd = data.revenue.cost_usd; const margin = revenueUsd > 0 ? ((revenueUsd - costUsd) / revenueUsd * 100).toFixed(1) : '–'; @@ -52,13 +52,13 @@ export default function AnalyticsTab() { - +
- - - + + + 0 ? `${margin}%` : '—'} sub="(revenue − cost) / revenue" />
{/* Daily purchases chart */}