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:
parent
72e8dadb20
commit
783a89e825
3 changed files with 22 additions and 12 deletions
|
|
@ -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),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue