From 05db7022612dbd20f0e922e388be5995769dcb88 Mon Sep 17 00:00:00 2001 From: smakosh Date: Sat, 18 Jul 2026 12:33:07 +0200 Subject: [PATCH] feat(admin): split reset pass revenue, 2-row KPIs Break Reset Pass sales out of the DevPass gross revenue split into their own line on the admin dashboard total revenue card. They remain counted in the gross revenue total; the DevPass split now covers subscription payments only, so the splits stay disjoint and sum to the total. Also lay out the DevPass KPI cards as a 2-row grid. Claude-Session: https://claude.ai/code/session_017Xbd48rpcVQWW2kx4vFiJ9 --- .../admin-metrics-gross-revenue.spec.ts | 16 +++++++- apps/api/src/routes/admin.ts | 38 +++++++++++++++++-- apps/api/src/utils/devpass-filter.ts | 9 +++++ ee/admin/src/app/devpass/page.tsx | 2 +- ee/admin/src/app/page.tsx | 6 +++ 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/apps/api/src/routes/admin-metrics-gross-revenue.spec.ts b/apps/api/src/routes/admin-metrics-gross-revenue.spec.ts index 4b4b7feb54..bcf8fc8b9a 100644 --- a/apps/api/src/routes/admin-metrics-gross-revenue.spec.ts +++ b/apps/api/src/routes/admin-metrics-gross-revenue.spec.ts @@ -16,6 +16,7 @@ interface AdminMetricsResponse { grossRevenue: number; grossCreditsRevenue: number; grossDevpassRevenue: number; + grossResetPassRevenue: number; grossChatPlansRevenue: number; grossProSubscriptionsRevenue: number; } @@ -97,6 +98,16 @@ describe("admin /metrics — gross revenue splits", () => { stripeInvoiceId: "inv_gross_dev_2", createdAt: new Date("2026-02-04T00:00:00Z"), }, + // Reset Pass: one-time PaymentIntent purchase, no invoice. Counted in + // its own split, not in DevPass subscription revenue. + { + organizationId: DEVPASS_ORG_ID, + type: "dev_plan_reset_pass", + amount: "9", + creditAmount: null, + status: "completed", + createdAt: new Date("2026-01-07T00:00:00Z"), + }, // Chat Plan: $10 paid for a plan that includes $150 of virtual // credits — the $150 allowance must never count as revenue. { @@ -134,12 +145,13 @@ describe("admin /metrics — gross revenue splits", () => { // Gross = positive Stripe amounts, refunds ignored: $21 + $11 credits. expect(body.grossCreditsRevenue).toBe(32); - // $20 start + $20 second-cycle renewal. + // $20 start + $20 second-cycle renewal — the Reset Pass is NOT in here. expect(body.grossDevpassRevenue).toBe(40); + expect(body.grossResetPassRevenue).toBe(9); expect(body.grossChatPlansRevenue).toBe(10); // Legacy subscription on a non-devpass org. expect(body.grossProSubscriptionsRevenue).toBe(50); - expect(body.grossRevenue).toBe(132); + expect(body.grossRevenue).toBe(141); // The credits-economy metrics must exclude ALL plan rows: chat plan // creditAmount ($150) is a virtual allowance, not revenue. diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index 9fa87517de..b541118edc 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -10,6 +10,7 @@ import { adminMiddleware } from "@/middleware/admin.js"; import { getStripe } from "@/routes/payments.js"; import { CHAT_PLAN_TX_TYPES, + DEV_PLAN_SUBSCRIPTION_TX_TYPES, DEV_PLAN_TX_TYPES, firstRowPerInvoiceFilter, LEGACY_DEV_PLAN_TX_TYPES, @@ -105,6 +106,7 @@ const adminMetricsSchema = z.object({ grossRevenue: z.number(), grossCreditsRevenue: z.number(), grossDevpassRevenue: z.number(), + grossResetPassRevenue: z.number(), grossChatPlansRevenue: z.number(), grossProSubscriptionsRevenue: z.number(), }); @@ -846,8 +848,10 @@ admin.openapi(getMetrics, async (c) => { const grossCreditsRevenue = Number(grossCreditsRow?.value ?? 0); - // DevPass: dev plan payments (+ legacy `subscription_*` rows on devpass - // orgs), deduplicated per Stripe invoice. Mirrors /admin/devpass/timeseries. + // DevPass: dev plan subscription payments (+ legacy `subscription_*` rows on + // devpass orgs), deduplicated per Stripe invoice. Mirrors + // /admin/devpass/timeseries. One-time Reset Pass purchases are reported as + // their own split below. const [grossDevpassRow] = await db .select({ value: @@ -865,7 +869,7 @@ admin.openapi(getMetrics, async (c) => { eq(tables.transaction.status, "completed"), eq(tables.organization.kind, "devpass"), inArray(tables.transaction.type, [ - ...DEV_PLAN_TX_TYPES, + ...DEV_PLAN_SUBSCRIPTION_TX_TYPES, ...LEGACY_DEV_PLAN_TX_TYPES, ]), firstRowPerInvoiceFilter([ @@ -878,6 +882,32 @@ admin.openapi(getMetrics, async (c) => { const grossDevpassRevenue = Number(grossDevpassRow?.value ?? 0); + // Reset Passes: one-time PaymentIntent purchases on devpass orgs — no + // invoice, so no dedup needed. Gross like the other splits (refunds are + // separate `credit_refund` rows and not netted out here). + const [grossResetPassRow] = await db + .select({ + value: + sql`COALESCE(SUM(CAST(${tables.transaction.amount} AS NUMERIC)), 0)`.as( + "value", + ), + }) + .from(tables.transaction) + .innerJoin( + tables.organization, + eq(tables.transaction.organizationId, tables.organization.id), + ) + .where( + and( + eq(tables.transaction.status, "completed"), + eq(tables.organization.kind, "devpass"), + eq(tables.transaction.type, "dev_plan_reset_pass"), + transactionDateFilter, + ), + ); + + const grossResetPassRevenue = Number(grossResetPassRow?.value ?? 0); + // Chat Plans: plan payments on chat orgs, deduplicated per Stripe invoice. // Mirrors /admin/chat-plans/timeseries. const [grossChatPlansRow] = await db @@ -936,6 +966,7 @@ admin.openapi(getMetrics, async (c) => { const grossRevenue = grossCreditsRevenue + grossDevpassRevenue + + grossResetPassRevenue + grossChatPlansRevenue + grossProSubscriptionsRevenue; @@ -960,6 +991,7 @@ admin.openapi(getMetrics, async (c) => { grossRevenue, grossCreditsRevenue, grossDevpassRevenue, + grossResetPassRevenue, grossChatPlansRevenue, grossProSubscriptionsRevenue, }); diff --git a/apps/api/src/utils/devpass-filter.ts b/apps/api/src/utils/devpass-filter.ts index 16ee564b4a..56426a47cd 100644 --- a/apps/api/src/utils/devpass-filter.ts +++ b/apps/api/src/utils/devpass-filter.ts @@ -45,6 +45,15 @@ export const DEV_PLAN_TX_TYPES = [ "dev_plan_reset_pass", ] as const; +// Subscription-billed dev plan rows only — excludes one-time Reset Pass +// purchases so they can be reported as their own revenue split. +export const DEV_PLAN_SUBSCRIPTION_TX_TYPES = [ + "dev_plan_start", + "dev_plan_upgrade", + "dev_plan_downgrade", + "dev_plan_renewal", +] as const; + // Pre-rename rows for what is now a dev plan. The same `subscription_*` types // are STILL written today for non-personal org Pro subs, so always pair them // with `organization.kind = 'devpass'` to avoid counting org Pro revenue. diff --git a/ee/admin/src/app/devpass/page.tsx b/ee/admin/src/app/devpass/page.tsx index 1459dc8b6f..b6a942efed 100644 --- a/ee/admin/src/app/devpass/page.tsx +++ b/ee/admin/src/app/devpass/page.tsx @@ -481,7 +481,7 @@ export default async function DevpassPage({ -
+
diff --git a/ee/admin/src/app/page.tsx b/ee/admin/src/app/page.tsx index 08ae696f7c..d655d5efc0 100644 --- a/ee/admin/src/app/page.tsx +++ b/ee/admin/src/app/page.tsx @@ -320,6 +320,12 @@ export default async function Page({ label: "DevPass", value: currencyFormatter.format(metrics.grossDevpassRevenue), }, + { + label: "Reset passes", + value: currencyFormatter.format( + metrics.grossResetPassRevenue, + ), + }, { label: "Chat plans", value: currencyFormatter.format(