Customer Billing
Customer billing separates what customers can see from what operators can investigate.
On this page
Customer and operator surfaces
| Surface | What it shows | Notes |
|---|---|---|
| /billing/history | Customer-owned purchases, payments, invoices, refunds, and subscription changes in one activity container. | Uses normalized activity rows and does not expose raw webhook payloads or operator controls. |
| /api/billing/portal | Provider-hosted portal launch for the signed-in customer. | Uses the active provider when a customer record exists. |
| /billing/customers | Operator list of provider customers. | Super-admin oriented customer operations. |
| /billing/customers/[providerCustomerId] | Customer ledger detail, subscriptions, invoices, charges, refunds, disputes, entitlements, and webhooks. | Provider capability differences are shown in UI. |
| /api/system/billing/export | Customer CSV and Stripe invoice CSV exports. | Invoice export is Stripe-only. |
Code-backed customer billing map
Customer-facing billing activity
Signed-in customers see account-owned normalized billing activity, not raw provider payloads.
src/app/(dashboard)/billing/page.tsxsrc/features/billing/server/queries/activity.tssrc/features/billing/ui/hub/activity.tsx
Portal access
Portal launch uses the active provider and current customer record through a server service.
src/app/api/billing/portal/route.tssrc/features/billing/server/services/portal.tssrc/features/billing/server/providers/stripe/portal.tssrc/features/billing/server/providers/lemonsqueezy/portal.ts
Operator investigation
Customer detail pages, sync, refunds, and exports are admin-oriented workflows with provider capability differences.
src/app/(dashboard)/billing/customers/[providerCustomerId]/page.tsxsrc/features/billing/server/queries/customer-history.tssrc/features/billing/server/services/customer-sync.tssrc/features/billing/server/services/admin-refund.tssrc/features/billing/server/export/service.ts
Customer billing support flow
- 1
Start from local history
Customer and operator pages read projected subscriptions, orders, invoices, charges, refunds, disputes, and entitlements from local tables.
src/features/billing/server/queries/current-profile-history.tssrc/features/billing/server/queries/customer-history.ts
- 2
Use portal for customer changes
The portal service resolves the active provider and customer before returning a provider-hosted URL.
src/features/billing/server/services/portal.ts
- 3
Sync provider state deliberately
Provider refresh and refunds stay behind admin services so support actions remain auditable and provider-aware.
src/features/billing/server/services/customer-sync.tssrc/features/billing/server/services/admin-refund.ts
- 4
Export from local projections
CSV exports are assembled from local customer and invoice rows after authorization checks.
src/features/billing/server/export/request/auth.tssrc/features/billing/server/export/service.ts
Portal request schema
Source: src/features/billing/server/services/portal.ts
Portal requests accept only relative return paths so provider redirects stay inside the app.
const PortalBodySchema = z.object({
return_path: z
.string()
.trim()
.max(300)
.refine((value) => value.startsWith("/") && !value.startsWith("//"), {
message: "return_path must be a relative path",
})
.optional(),
source: z.string().trim().max(80).optional().default("dashboard_billing_hub"),
});Support workflow
- Start from the customer detail page when investigating billing support.
- Use Stripe sync only when the active provider supports it.
- Use local projections as the dashboard source of truth after webhooks arrive.
- Keep refunds and disputes behind super-admin checks.