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’s nextCursor. nextCursor is 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: succeeded is reported as completed, with completedAt. Only failed payments carry a failureReason; an unknown stored code becomes unknown.
  • Invoices have one line item for the subscription’s plan, for example Indie plan, 1 month, at the stored amount due. issuedAt is 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 cancelSubscription sets cancel_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, with subscription.cancelAtPeriodEnd: true.
  • One tenant audit event is written: billing.subscription.cancel_requested, outcome: success, target subscription. 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