import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { forbidden } from "~/server/errors"; import { limitWrite, limitAccountWrite } from "~/server/limits"; import { billingAccountFor, requireSiteAdmin, topUsage } from "~/server/metering/access"; import { plans, setPlan, setSpendingLimit } from "~/server/metering/quota"; import { MAX_SPENDING_LIMIT_CENTS } from "~/lib/plans"; import { periodOf } from "~/server/metering/record"; import { usageSummary } from "~/server/metering/summary"; import { creditLedger } from "~/server/metering/credits"; import { setCustomDomainsGrant } from "~/server/billing/domain-addon"; import { BYOK_PROVIDERS, endpointSummary } from "~/server/ai/endpoint"; import { PRO_PRICE_CENTS, creditPoolFor } from "~/lib/pricing"; import { meterDef } from "~/lib/meters"; import type { MeterId } from "~/lib/meters"; import { rateLabel } from "~/lib/pricing-display"; import { accountDisableState, disabledNotice } from "~/server/account-disable"; import { createDomainAddonCheckout, createPortalSession, createSubscriptionCheckout, createTopupCheckout, followBillingPlan, stripeBillingView, } from "~/server/billing/checkout"; const period = z .string() .regex(/^\d{4}-(0[1-9]|1[0-2])$/) .optional(); const org = z.string().min(1).max(100).nullable(); export const getBilling = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(z.object({ org, period })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); const account = await billingAccountFor(viewer, data.org); if (!account.canView) throw forbidden("Only owners can view billing for this organization"); const [summary, stripe, ledger, aiEndpoint, disabledState] = await Promise.all([ usageSummary(account.accountId, data.period ?? periodOf()), stripeBillingView(account.accountId), account.canView ? creditLedger(account.accountId, 20) : [], account.canView ? endpointSummary("account", account.accountId) : null, accountDisableState(account.accountId), ]); const all = plans(); return { account, disabled: disabledState ? disabledNotice(disabledState.reason) : null, summary, stripe, ledger, ai: { endpoint: aiEndpoint, providers: BYOK_PROVIDERS }, plans: { free: { label: all.free.label, included: all.free.included }, pro: { label: all.pro.label, included: all.pro.included, priceCents: PRO_PRICE_CENTS }, }, }; }); /** Starts Checkout for the Pro subscription. Owners only; the webhook applies the plan. */ export const startProCheckout = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(z.object({ org })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); return createSubscriptionCheckout(await billingAccountFor(viewer, data.org), viewer); }); /** Starts Checkout for a prepaid pack from STRIPE_TOPUP_PRICES. */ export const startTopupCheckout = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(z.object({ org, pack: z.string().min(1).max(40) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); return createTopupCheckout(await billingAccountFor(viewer, data.org), viewer, data.pack); }); /** Starts Checkout for the per-domain custom domain add-on (STRIPE_PRICE_CUSTOM_DOMAIN). */ export const startDomainAddonCheckout = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(z.object({ org })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); return createDomainAddonCheckout(await billingAccountFor(viewer, data.org), viewer); }); /** A Customer Portal session to manage or cancel the subscription and update the card. */ export const openBillingPortal = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(z.object({ org })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); // Cancelling or updating the card is self-service; the portal has no plan changes or new purchases. await limitAccountWrite(viewer); return createPortalSession(await billingAccountFor(viewer, data.org)); }); export const updateSpendingLimit = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(z.object({ org, cents: z.number().int().min(0).max(MAX_SPENDING_LIMIT_CENTS).nullable() })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitAccountWrite(viewer); const account = await billingAccountFor(viewer, data.org); if (!account.canEdit) throw forbidden("Only organization owners can change the spending limit"); await setSpendingLimit(account.accountId, data.cents); return { ok: true }; }); export const getAdminBilling = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(z.object({ period })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); requireSiteAdmin(viewer); const current = data.period ?? periodOf(); return { period: current, accounts: await topUsage(current, plans()) }; }); export const adminSetPlan = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(z.object({ accountId: z.string().min(1).max(100), plan: z.enum(["free", "pro", "billing"]) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); requireSiteAdmin(viewer); await limitWrite(viewer); if (data.plan === "billing") await followBillingPlan(data.accountId); else await setPlan(data.accountId, data.plan, "admin"); return { ok: true }; }); /** Grants or revokes custom domains without the paid add-on (comped accounts, instances without billing). */ export const adminGrantCustomDomains = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(z.object({ accountId: z.string().min(1).max(100), granted: z.boolean() })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); requireSiteAdmin(viewer); await limitWrite(viewer); await setCustomDomainsGrant(data.accountId, data.granted); return { ok: true }; }); /** What the upsell dialog shows for one meter: its price, what each plan includes, and how to get more. */ export const getUpsellInfo = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(z.object({ org, meter: z.string().min(1).max(60) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); const account = await billingAccountFor(viewer, data.org); const meter = meterDef(data.meter); const all = plans(); const [summary, stripe] = await Promise.all([ usageSummary(account.accountId), stripeBillingView(account.accountId), ]); const pool = meter ? (creditPoolFor(meter.id)?.pool ?? null) : null; const usage = summary.meters.find((entry) => entry.meter === data.meter) ?? null; return { account, meter: meter ? { id: meter.id, label: meter.label, unit: meter.unit } : null, rate: meter ? rateLabel(meter.id) : null, plan: summary.plan, spendingLimitCents: account.canView ? summary.spendingLimitCents : null, used: account.canView ? (usage?.quantity ?? null) : null, included: { free: meter ? (all.free.included[meter.id as MeterId] ?? null) : null, pro: meter ? (all.pro.included[meter.id as MeterId] ?? null) : null, }, pro: { label: all.pro.label, priceCents: PRO_PRICE_CENTS, available: stripe.proAvailable }, billingEnabled: stripe.enabled, packs: pool ? stripe.packs.filter((pack) => pack.pool === pool) : [], }; });