Files
podcastdistributiona/lib/admin/metrics.ts
T

83 lines
3.0 KiB
TypeScript

import { prisma } from "@/lib/db";
import { PLANS, type PlanKey } from "@/lib/billing/plans";
import { rangeWindow, pctDelta, type Range } from "./range";
function mrrOf(subs: { plan: string }[]): number {
return subs.reduce((sum, s) => sum + (PLANS[s.plan as PlanKey]?.priceMonthly ?? 0), 0);
}
export interface Overview {
mrr: number;
arr: number;
arpu: number;
paying: number;
activeSubs: number;
totalUsers: number;
signups: number;
signupsDelta: number | null;
episodes: number;
episodesDelta: number | null;
aiSpend: number;
aiSpendDelta: number | null;
churned: number;
successRate: number;
failedEpisodes: number;
tierCounts: Record<string, number>;
providerSplit: { stripe: number; paypal: number; comp: number };
}
export async function getOverview(range: Range): Promise<Overview> {
const w = rangeWindow(range);
const [activeSubs, signups, prevSignups, episodes, prevEpisodes, failed, spend, prevSpend, churned, totalUsers] =
await Promise.all([
prisma.subscription.findMany({
where: { status: { in: ["active", "trialing"] } },
select: { plan: true, provider: true },
}),
prisma.user.count({ where: { createdAt: { gte: w.since } } }),
prisma.user.count({ where: { createdAt: { gte: w.prevSince, lt: w.prevUntil } } }),
prisma.episode.count({ where: { createdAt: { gte: w.since } } }),
prisma.episode.count({ where: { createdAt: { gte: w.prevSince, lt: w.prevUntil } } }),
prisma.episode.count({ where: { status: "FAILED", createdAt: { gte: w.since } } }),
prisma.aiCostLog.aggregate({ _sum: { costUsd: true }, where: { createdAt: { gte: w.since } } }),
prisma.aiCostLog.aggregate({
_sum: { costUsd: true },
where: { createdAt: { gte: w.prevSince, lt: w.prevUntil } },
}),
prisma.subscription.count({ where: { status: "canceled", updatedAt: { gte: w.since } } }),
prisma.user.count(),
]);
const mrr = mrrOf(activeSubs);
const paying = activeSubs.filter((s) => s.plan !== "free").length;
const aiSpend = Number(spend._sum.costUsd ?? 0);
const prevAiSpend = Number(prevSpend._sum.costUsd ?? 0);
const tierCounts: Record<string, number> = { free: 0, creator: 0, pro: 0, agency: 0 };
for (const s of activeSubs) if (s.plan in tierCounts) tierCounts[s.plan]++;
return {
mrr,
arr: mrr * 12,
arpu: paying > 0 ? Math.round(mrr / paying) : 0,
paying,
activeSubs: activeSubs.length,
totalUsers,
signups,
signupsDelta: pctDelta(signups, prevSignups),
episodes,
episodesDelta: pctDelta(episodes, prevEpisodes),
aiSpend,
aiSpendDelta: pctDelta(aiSpend, prevAiSpend),
churned,
successRate: episodes > 0 ? Math.round(((episodes - failed) / episodes) * 100) : 100,
failedEpisodes: failed,
tierCounts,
providerSplit: {
stripe: activeSubs.filter((s) => s.provider === "stripe").length,
paypal: activeSubs.filter((s) => s.provider === "paypal").length,
comp: activeSubs.filter((s) => s.provider === "comp").length,
},
};
}