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.