Story 2173 - pramaan-admin

Staff can retune every commercial number from the admin pricing page

This story adds a staff-only /pricing page where staff view and edit every commercial number — the global billing.pricing_config knobs and each billing.plan row — without a deploy (RFD-BILL-001 §14, pramaan-docs ADR-151 / ADR-158). The money math, range validation, and audit all live behind the S2 config routes in pramaan-functions. The admin only proxies them through src/lib/gateway.ts.

Flow

The page is a server component. On load it reads the global config and the full plan list in parallel through billingConfigGateway (GET /admin/billing/pricing-config and GET /admin/billing/plans), each parsed with assertContract against the generated schemas in src/gen/. A skeleton covers the suspense boundary.

One reusable ConfigForm (react-hook-form + a zod schema built from a field spec) renders the global config and one card per plan. On save it sends only the dirty fields as a sparse PATCH via the updatePricingConfig / updateBillingPlan server actions — decimals as strings (precision preserved), integers as numbers, blanked nullable fields as null. Amounts are INR (₹).

Server-confirmed, admin-gated writes

Writes are server-confirmed only: the form rebases its baseline from the PATCH response the server returns and shows a “applies to new orders and runs only” toast — no optimistic cache flip. The (admin) layout gates the page to active staff via checkAdminMe; the S2 API narrows PATCH to platform-admin (PlatformAdminDbDep), and its 403 surfaces as a form-level error rather than a silent failure.

Acceptance evidence

Criterion Evidence State
Staff view + edit every commercial number src/components/pricing/pricing-view.tsx specs all global knobs (markup, FX, GST, min top-up, wallet floor, cap warn) and every plan field (seat price, bonus, allowance, storage tiers, expiry windows, flags), rendered by config-form.tsx. done
Save persists via the S2 API src/lib/actions/pricing.ts + src/lib/billing/gateway.ts proxy PATCH /admin/billing/pricing-config and PATCH /admin/billing/plans/{plan_key}; contracts from src/gen/. done
Audit / confirmation shown; new orders only Backend audits each old→new PATCH; the UI rebases from the confirmed response and toasts “applies to new orders and runs only”. done
Staff-only Page under the (admin) group gated by checkAdminMe; PATCH narrowed to platform-admin by the S2 route, 403 surfaced as a form error. done
No client-side money math; contracts generated The UI only sends edits; refreshed openapi/admin-api.json + regenerated src/gen (no hand-written mirrors). Unit test covers the dirty-diff/patch transform. done
Local verify gate pnpm exec tsc --noEmit, pnpm run lint, pnpm run test, and pnpm run build pass locally. done

Scope note: the proxied S2 config routes ship on the functions branch issue-2163-s2-plan-config-api and are not yet merged to functions main. This page, gateway, actions, and generated client are the real live path — it activates once S2 merges and deploys to staging. Until then /pricing returns upstream 404 at runtime; no mock data is wired.