14 file(s), +392 / -145 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +2 | -0 |
| messages/hi.json | Config | +2 | -0 |
| openapi/app-api.json | Config | +2 | -1 |
| openapi/permission-map.json | Config | +7 | -0 |
| src/app/api/internal/credits/razorpay/order/route.test.ts | App route | +57 | -0 |
| src/app/api/internal/credits/{topup => razorpay/order}/route.ts | App route | +5 | -6 |
| src/components/settings/billing/credits-page.test.tsx | Component | +87 | -70 |
| src/components/settings/billing/credits-page.tsx | Component | +62 | -48 |
| src/components/settings/billing/razorpay-checkout.ts | Component | +89 | -0 |
| src/gen/schemas/agentSessionTurnRequest.json | Generated contract | +1 | -1 |
| src/gen/types/AgentSessionTurnRequest.ts | Generated contract | +2 | -1 |
| src/gen/zod/agentSessionTurnRequestSchema.ts | Generated contract | +1 | -1 |
| src/lib/credits/gateway-razorpay.test.ts | Library | +55 | -0 |
| src/lib/credits/gateway.ts | Library | +20 | -17 |
The firm billing "Buy credits" button called the dead Stripe route
POST /app/firm/credits/topup, which returns
503 "Stripe is not configured" — Stripe was never
provisioned. PRAMAAN moved to Razorpay, whose backend
(order create + S8 signature-verified webhook mint) is already live on
staging. This story swaps the app onto the Razorpay Checkout modal.
POST /app/firm/credits/razorpay/order
(useAppCreateRazorpayTopupOrder,
razorpayTopupOrderResponseSchema) were regenerated in a
prior commit; this story consumes them.
creditsGateway.createRazorpayOrderWithResponseHeaders
(src/lib/credits/gateway.ts) POSTs
{ amount_inr } to the razorpay order route and
assertContract-validates the
RazorpayTopupOrderResponse. The dead Stripe
createTopupWithResponseHeaders method was removed.
src/app/api/internal/credits/razorpay/order/route.ts
validates the body with the generated zod schema and forwards Pramaan
headers, mirroring the old topup proxy. The dead
credits/topup/route.ts was deleted; the browser→server map
needs no change (the existing /app/firm/credits/ →
/api/internal/credits/ rule already covers it).
src/components/settings/billing/razorpay-checkout.ts loads
checkout.js from the CDN once (idempotent) and exposes a
typed openRazorpayCheckout({ ...order, onSuccess, onDismiss }).
No npm SDK; the modal script is the standard integration.
credits-page.tsx) — "Buy
credits" now: (1) creates the order at the ₹1/credit peg
(amount_inr = credits), (2) opens the Razorpay modal,
(3) renders the "₹X + Y% GST = ₹Z" breakdown from the
order response, (4) on payment success polls firm credits
(5 × 1.5s) so the webhook-minted balance lands, (5) on
dismiss shows a friendly cancel notice. The Stripe
checkout_url redirect and the
?topup=success/cancel return handling are gone (a modal has
no redirect).
Credits.checkoutDescription and
Credits.breakdown added to en + hi.
gateway-razorpay.test.ts — asserts the upstream path/body
and a contract violation on shape drift.
credits/razorpay/order/route.test.ts — forwards a valid
amount_inr; rejects a bad payload with 422 before hitting
the gateway.
credits-page.test.tsx — order created at ₹1/credit, modal
opened with the right args, GST breakdown shown, success notice +
credit poll, cancel notice on dismiss, order-create error surfaced.
pnpm lint — clean (one pre-existing <img> warning in add-matter, untouched).pnpm typecheck — clean.pnpm build — green; /api/internal/credits/razorpay/order present, credits/topup gone.
On-staging check (AC): a firm-admin on the billing page
clicks "Buy credits" → the Razorpay Checkout modal opens against
rzp_test with the correct amount (no 503/500). After a test
payment, the S8 webhook mints the credits and the polled balance reflects
them. The webhook mint path is backend-owned (pramaan-functions) and is
the runtime boundary this story depends on, not re-proven here.