fix(analytics): correct revenue, persona count, and gross margin

- Store amount_usd in Stripe webhook transaction (was only storing credits)
- Analytics endpoint: sum amount_usd for real USD revenue instead of credits
- Persona count: query personas collection directly (not credit transactions
  which counted transactions, not individual personas — batch creation
  of 3 at once appeared as count=1)
- Frontend: Revenue card now shows USD not credits; Gross Margin uses
  USD revenue so calculation is dimensionally correct

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-25 17:10:59 +01:00
parent 72e8dadb20
commit 783a89e825
3 changed files with 22 additions and 12 deletions

View file

@ -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),
},

View file

@ -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},

View file

@ -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 <div className="flex justify-center py-12"><Loader2 className="h-6 w-6 animate-spin" /></div>;
if (!data) return <p className="text-muted-foreground">Failed to load analytics.</p>;
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() {
<StatCard icon={Users} title="Total Users" value={data.users.total} sub={`+${data.users.new_in_period} this period`} />
<StatCard icon={Zap} title="FG Runs" value={data.activity.focus_group_runs} sub="focus group sessions" />
<StatCard icon={Users} title="Personas Created" value={data.activity.personas_created} />
<StatCard icon={DollarSign} title="Credits Sold" value={data.revenue.credits_sold} sub={`${data.revenue.purchase_count} purchases`} />
<StatCard icon={DollarSign} title="Credits Sold" value={data.revenue.credits_sold} sub={`${data.revenue.purchase_count} purchase${data.revenue.purchase_count !== 1 ? 's' : ''}`} />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<StatCard icon={TrendingUp} title="Cost (USD)" value={`$${costUsd.toFixed(2)}`} sub="AI inference cost" />
<StatCard icon={DollarSign} title="Revenue (credits)" value={revenueUsd} sub="credits sold = $" />
<StatCard icon={BarChart2} title="Gross Margin" value={`${margin}%`} sub="revenue cost / revenue" />
<StatCard icon={TrendingUp} title="AI Cost (USD)" value={`$${costUsd.toFixed(2)}`} sub="AI inference cost" />
<StatCard icon={DollarSign} title="Revenue (USD)" value={`$${revenueUsd.toFixed(2)}`} sub={`${data.revenue.credits_sold} credits sold via Stripe`} />
<StatCard icon={BarChart2} title="Gross Margin" value={revenueUsd > 0 ? `${margin}%` : '—'} sub="(revenue cost) / revenue" />
</div>
{/* Daily purchases chart */}