Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions apps/api/src/routes/admin-metrics-gross-revenue.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface AdminMetricsResponse {
grossRevenue: number;
grossCreditsRevenue: number;
grossDevpassRevenue: number;
grossResetPassRevenue: number;
grossChatPlansRevenue: number;
grossProSubscriptionsRevenue: number;
}
Expand Down Expand Up @@ -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.
{
Expand Down Expand Up @@ -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.
Expand Down
38 changes: 35 additions & 3 deletions apps/api/src/routes/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
});
Expand Down Expand Up @@ -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:
Expand All @@ -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([
Expand All @@ -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<number>`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
Expand Down Expand Up @@ -936,6 +966,7 @@ admin.openapi(getMetrics, async (c) => {
const grossRevenue =
grossCreditsRevenue +
grossDevpassRevenue +
grossResetPassRevenue +
grossChatPlansRevenue +
grossProSubscriptionsRevenue;

Expand All @@ -960,6 +991,7 @@ admin.openapi(getMetrics, async (c) => {
grossRevenue,
grossCreditsRevenue,
grossDevpassRevenue,
grossResetPassRevenue,
grossChatPlansRevenue,
grossProSubscriptionsRevenue,
});
Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/utils/devpass-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion ee/admin/src/app/devpass/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ export default async function DevpassPage({
</Suspense>
</header>

<section className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
<section className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
<div className="rounded-lg border border-border/60 bg-card p-4">
<div className="flex items-center gap-2 text-xs uppercase tracking-wide text-muted-foreground">
<Users className="h-3.5 w-3.5" />
Expand Down
6 changes: 6 additions & 0 deletions ee/admin/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading