AUTH4D-46 console billing screen and checkout return page
The console adds a Billing section at /projects/{tenantId}/billing (navigation group
“Project”) and the checkout return page at /billing/return. Both live in
apps/console/src/features/billing/. Every amount, limit and entitlement shown comes from the API.
The console never computes a price. Checkout sends only the plan id and receives a redirect URL.
BFF proxy additions
apps/console/worker/app.ts now also proxies these management routes, with the same session,
token and same-origin write checks as the other management routes:
| Console request | Upstream (AUTH4D ticket) | Who can use it in the UI |
|---|---|---|
GET /api/management/tenants/{tenantId}/usage |
AUTH4D-39 | any member |
GET /api/management/tenants/{tenantId}/billing/plans |
AUTH4D-45 | any member |
GET /api/management/tenants/{tenantId}/billing/subscription |
AUTH4D-45 | any member |
GET /api/management/tenants/{tenantId}/billing/invoices |
AUTH4D-45 | owner, admin |
GET /api/management/tenants/{tenantId}/billing/payments |
AUTH4D-45 | owner, admin |
POST /api/management/tenants/{tenantId}/billing/checkout |
AUTH4D-40 | owner, admin |
POST /api/management/tenants/{tenantId}/billing/subscription/cancel |
AUTH4D-45 | owner, admin |
- The
Idempotency-Keyrequest header is forwarded only when it matches[A-Za-z0-9_.:-]{16,128}. Retry-AfterandIdempotent-Replayedresponse headers are passed back withContent-Type,WWW-AuthenticateandX-Request-Id. No other upstream headers are passed back.- Other billing paths, including the Stitch webhook, still return
404from the BFF.
Billing screen
- Role. The screen reads
rolefromGET /api/management/tenants/{tenantId}. Owners and admins can manage billing. Any other or unknown role gets a read-only view, and the API still authorizes every request. Read-only users see the current plan, usage and the plan catalog as a list. They do not see the picker, checkout, cancel, invoices or payments, and none of those requests is sent. - Current plan. This shows
GET billing/subscription: plan, status, renewal or end date and entitlements. A past-due subscription and a pending cancellation each show a notice. - Usage.
GET usageis shown as a captioned table: monthly active users and enabled applications, each with its plan limit and whether it is over that limit. A grace period shows a notice. MAU over the limit is informational only. - Plan picker. A
fieldsetandlegendradio group lists the catalog fromGET billing/planswith display-only prices (amountMinorformatted for the currency). The free plan and the current plan cannot be selected. If a paid plan is already entitled, the picker is disabled and explains why (the API would answer409 subscription_exists). - Checkout.
POST billing/checkoutsends{ "planId": "indie" | "studio" }and anIdempotency-Key(a random UUID). A retry of the same plan reuses that key, as AUTH4D-40 requires after502 payment_provider_unavailable. Aftercheckout_closed,idempotency_key_reusedorsubscription_existsthe key is dropped. The browser followsredirectUrlonly if it is HTTPS onstitch.moneyor a subdomain, or loopback HTTP for the local mock, with no credentials. Any other URL is treated as an invalid response. - Invoices and payments. These are captioned tables, newest first. “Load more” follows
nextCursor(limit=25) and appends rows. A live status line reports how many rows are shown. - Cancel. A modal dialog confirms “Cancel at period end” and sends
POST billing/subscription/cancelwith{ "atPeriodEnd": true }. On success the returned state replaces the current plan, a status notice is announced, and focus moves to the “Current plan” heading. - Error codes from AUTH4D-40/45 (
billing_unavailable,checkout_in_progress,payment_provider_unavailable,no_active_subscription,subscription_changed,insufficient_role, …) get specific messages, shown with their request ID.
Checkout return page
AUTH4D-40 builds the return URL as {CONSOLE_ORIGIN}/billing/return?tenant_id=…&payment_id=….
- A
tenant_idthat is not a project id is a “Page not found”. A missing or malformedpayment_idshows an explanation with a link to billing. The project must be in the user’s project list, as for every project route. - The page is titled “Payment status” and highlights Billing in the navigation.
- A
statusquery value appended by the provider is used only for the waiting message (for example “you left the payment page”). It never decides the result. - The page polls
GET billing/payments?limit=10(up to 2 pages per poll) until the payment iscompleted,failed,canceledorexpired. The first wait is 2 s, growing by 25% per poll up to 8 s. Rate limits and transient errors double the interval. Polling stops after 90 s, which keeps it far below the 120-per-minute management budget.401,403,404,400and invalid responses stop it at once. - Results: Payment complete (and the new plan from
GET billing/subscription), Payment failed (with the failure reason), Payment cancelled, Checkout expired, or Payment still processing after the time-out, which has a “Check again” button. Focus moves to the result heading. Leaving the page aborts polling.
Tests
tests/integration/auth4d-46.console-billing.test.ts(Vitest): the API client (checkout body and header, untrusted redirects, paths and cursors, roles), polling (final states, paging, back-off, time-out, permanent errors, abort), return-route parsing, BFF proxying, and static renders of the components (labels, table header scopes, read-only views, escaping).tests/e2e/auth4d-46.console-billing.spec.ts(Playwright, mocked BFF and mocked Stitch page, port 5196):pnpm exec playwright test -c tests/e2e/auth4d-46.playwright.config.ts.
Source: docs/api-spec/auth4d-46.md