AUTH4D-45 billing management reads and cancel at period end
apps/api/src/modules/billing/management.ts adds these routes to the billing feature. They live
under /management/tenants/{tenantId}/billing:
| Route | Scope | Roles | Response (AUTH4D-33 DTO) |
|---|---|---|---|
GET /plans |
tenant:read |
any project member | BillingPlanCatalogResponse |
GET /subscription |
tenant:read |
any project member | ManagementBillingSubscriptionResponse |
GET /invoices |
tenant:read |
owner, admin |
ManagementInvoiceListResponse |
GET /payments |
tenant:read |
owner, admin |
ManagementPaymentListResponse |
POST /subscription/cancel |
tenant:write |
owner, admin |
ManagementBillingSubscriptionResponse |
Every response is parsed through its strict contract schema before it is sent. Provider ids,
external references, billing account ids and raw failure codes are never returned. These routes
read and write D1 only, so they work when BILLING_ENABLED is off. Responses are no-store.
Checks, in order
| Step | Failure |
|---|---|
| Management token with the route scope (AUTH4D-17) | 401 unauthorized |
| Path tenant id | 404 not_found |
| Per-actor management budget (shared with checkout) | 429 rate_limited, Retry-After: 60 |
| Live membership in an active customer project | 403 forbidden |
| Role, for invoices, payments and cancel | 403 insufficient_role; audited as denied for cancel only |
| Query parameters or body | 400 invalid_request |
Every query binds the path tenant. A member of another project receives 403, never another tenant’s rows.
Current plan and subscription
GET /subscription returns the effective plan. It is the plan of the newest entitled subscription
(active or past_due), or free with subscription: null. entitlements always comes from the
server-side catalog. Pending checkouts and ended subscriptions are not reported here. Their
invoices and payments appear in the lists below.
Invoices and payment history
GET /invoices and GET /payments return the newest records first. They are ordered by
createdAt and then by id, both descending.
limit: 1–100, default 25.cursor: the previous page’snextCursor.nextCursoris omitted on the last page.
Pagination reuses the AUTH4D-19 user-admin keyset cursor (apps/api/src/modules/user-admin/pagination.ts).
The cursor is opaque and typed (billing_invoices or billing_payments), and it carries position
only. A cursor of the wrong kind is rejected with 400. So are unknown, repeated or oversized
parameters.
Mapping from stored records:
- Payments:
succeededis reported ascompleted, withcompletedAt. Onlyfailedpayments carry afailureReason; an unknown stored code becomesunknown. - Invoices have one line item for the subscription’s plan, for example
Indie plan, 1 month, at the stored amount due.issuedAtis the creation time. The 0005 schema does not yet record invoice periods. An unrecorded period is one billing interval starting when the invoice was paid, which is when the state machine starts the subscription period. For unpaid invoices it starts at creation.
Cancel at period end
POST /subscription/cancel accepts an empty body, {} or { "atPeriodEnd": true }
(CancelSubscriptionRequest). { "atPeriodEnd": false } returns 400: immediate cancellation is
not offered.
- No entitled subscription: 409
no_active_subscription. - Otherwise the billing repository’s
cancelSubscriptionsetscancel_at_period_end = 1. The state machine checks that the later expiry is a legal transition. Status, period and entitlements do not change until the period ends. - The response is the updated
ManagementBillingSubscriptionResponse, withsubscription.cancelAtPeriodEnd: true. - One tenant audit event is written:
billing.subscription.cancel_requested,outcome: success, targetsubscription. Its metadata holds the actor role, plan id, status and current period end. - Repeating the request returns 200 with the same state. It writes nothing and adds no audit event.
- A concurrent state change returns 409
subscription_changed.
No Stitch call is made. Stitch payment requests are one-off, so there is no provider-side mandate to cancel.
Logging
Each request writes one structured billing.management line. It holds the route, outcome, status,
duration, request id, tenant id and, for cancel, the subscription id. Tokens, bodies and amounts
are never logged.
Source: docs/api-spec/auth4d-45.md