83 lines
3.0 KiB
TypeScript
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,
|
|
},
|
|
};
|
|
}
|