33 file(s), +1243 / -622 lines
| Path | Role | + | - |
|---|---|---|---|
| openapi/app-api.json | Config | +78 | -24 |
| openapi/permission-map.json | Config | +1 | -1 |
| src/app/api/internal/credits/razorpay/order/route.test.ts | App route | +67 | -13 |
| src/app/api/internal/credits/razorpay/order/route.ts | App route | +59 | -8 |
| src/components/settings/billing/credits-page.test.tsx | Component | +5 | -5 |
| src/components/settings/billing/credits-page.tsx | Component | +5 | -7 |
| src/components/settings/billing/use-create-topup-order.ts | Component | +29 | -0 |
| src/gen/client/appCreateRazorpayTopupOrder.ts | Generated contract | +0 | -72 |
| src/gen/client/appQuoteRazorpayTopupOrder.ts | Generated contract | +72 | -0 |
| src/gen/client/index.ts | Generated contract | +1 | -1 |
| src/gen/hooks/index.ts | Generated contract | +4 | -4 |
| src/gen/hooks/useAppCreateRazorpayTopupOrder.ts | Generated contract | +0 | -156 |
| src/gen/hooks/useAppQuoteRazorpayTopupOrder.ts | Generated contract | +156 | -0 |
| src/gen/index.ts | Generated contract | +34 | -32 |
| src/gen/schemas/razorpayTopupNotes.json | Generated contract | +62 | -0 |
| src/gen/schemas/razorpayTopupOrderResponse.json | Generated contract | +0 | -59 |
| src/gen/schemas/razorpayTopupQuoteResponse.json | Generated contract | +110 | -0 |
| src/gen/types/AppCreateRazorpayTopupOrder.ts | Generated contract | +0 | -77 |
| src/gen/types/AppQuoteRazorpayTopupOrder.ts | Generated contract | +77 | -0 |
| src/gen/types/RazorpayTopupNotes.ts | Generated contract | +55 | -0 |
| src/gen/types/RazorpayTopupOrderResponse.ts | Generated contract | +0 | -53 |
| src/gen/types/RazorpayTopupQuoteResponse.ts | Generated contract | +51 | -0 |
| src/gen/types/index.ts | Generated contract | +15 | -14 |
| src/gen/zod/{appCreateRazorpayTopupOrderSchema.ts => appQuoteRazorpayTopupOrderSchema.ts} | Generated contract | +35 | -35 |
| src/gen/zod/index.ts | Generated contract | +14 | -13 |
| src/gen/zod/razorpayTopupNotesSchema.ts | Generated contract | +35 | -0 |
| src/gen/zod/razorpayTopupOrderResponseSchema.ts | Generated contract | +0 | -29 |
| src/gen/zod/razorpayTopupQuoteResponseSchema.ts | Generated contract | +37 | -0 |
| src/lib/credits/gateway-razorpay.test.ts | Library | +18 | -10 |
| src/lib/credits/gateway.ts | Library | +15 | -9 |
| src/lib/credits/razorpay-order.test.ts | Library | +91 | -0 |
| src/lib/credits/razorpay-order.ts | Library | +92 | -0 |
| src/lib/credits/topup-order.ts | Library | +25 | -0 |
Story-2192 wired the Razorpay Checkout modal, but the actual order-create
hung and returned 500. Root cause (verified 2026-07-09):
the pramaan-app-api Lambda sits in the default VPC with
no NAT, so it cannot reach api.razorpay.com —
Razorpay order-create was the first outbound HTTP the backend ever tried.
Founder decision: do not add NAT/egress; keep the backend
internet-free. Next.js (which has internet) places the Razorpay
order.
| Tier | Responsibility |
|---|---|
| Backend (pramaan-functions, Story-2193 — merged) |
POST /app/firm/credits/razorpay/order now
QUOTES only — prices the top-up under firm RLS and
returns RazorpayTopupQuoteResponse
(totalPaise, basePaise, gstPaise,
gstPct, credits, notes). No
egress, no Razorpay keys on the Lambda.
|
| App (pramaan-app — this story) | The Next route calls the backend quote, then places the Razorpay order server-side with keys from Next server env, and returns the Checkout shape the #2192 modal already consumes. |
openapi/app-api.json + permission-map.json)
was synced from functions main and src/gen
regenerated. RazorpayTopupOrderResponse →
RazorpayTopupQuoteResponse + RazorpayTopupNotes;
the operation renamed appCreate… →
appQuoteRazorpayTopupOrder.
creditsGateway.quoteRazorpayTopupWithResponseHeaders
(src/lib/credits/gateway.ts) POSTs the amount and
contract-asserts the RazorpayTopupQuoteResponse (HR-21).
src/lib/credits/razorpay-order.ts (server-only) places the
order at https://api.razorpay.com/v1/orders via basic auth
from RAZORPAY_KEY_ID/RAZORPAY_KEY_SECRET.
POST /api/internal/credits/razorpay/order now: parse →
backend quote → Razorpay egress → return
{ orderId, razorpayKeyId, amountPaise, currency, credits,
basePaise, gstPaise, gstPct }. 502 on egress failure, 500 when
keys are unset.
src/lib/credits/topup-order.ts holds the browser-order Zod
schema/type (BFF-owned, not a backend DTO — HR-22 permits a local
browser-only schema). The page consumes it via the hand-written
useCreateTopupOrder hook (the generated hook now mirrors the
quote, not the order).
The S8 mint webhook (unchanged) reads Razorpay order notes by
snake_case keys (firm_id,
credits, …) and treats every value as a string. But the
app-api quote emits notes in camelCase
(firmId, paiseCharged, …). Forwarding them
verbatim would drop firm_id and the webhook would silently
refuse to mint. So the BFF remaps to the exact shape the webhook
reads, mirroring the retired backend build_order_params:
notes = {
firm_id: String(notes.firmId),
credits: String(notes.credits),
paise_charged: String(notes.paiseCharged),
base_paise: String(notes.basePaise),
gst_paise: String(notes.gstPaise),
gst_pct: String(notes.gstPct),
ai_markup: String(notes.aiMarkup),
fx_usd_to_inr: String(notes.fxUsdToInr),
}
Amount, GST and notes are all computed by the backend under RLS — the
browser/BFF never set the price. The Razorpay secret lives in Next server
env only (never NEXT_PUBLIC, never logged). The webhook stays
the only credit-minting path.
pnpm typecheck, pnpm lint, pnpm build — green.fetch mocked (asserts snake_case stringified notes,
amount, basic auth), the route (quote → egress → Checkout shape, plus
422/500/502 paths), and the credits page.
RAZORPAY_KEY_ID/RAZORPAY_KEY_SECRET
added to local .env.local (rzp_test, from
pramaan/razorpay/staging). They must also be set as Vercel
server env vars for staging.