14. Payments
[← Shipping]/13-shipping/ · [Index]/00-index/
14. Payments
Verified against: services/medusa/src/modules/payment-razorpay/service.ts (source-read
directly), .ai/features/payments.md, and the payment-status table already built in
[Section 8]/08-order-management/#payment-status-values (not re-derived here — cross-referenced
instead of duplicated, so the two pages can’t drift apart).
INFO: Unlike most of this wiki, payments is a real, custom-built integration — a full Razorpay payment provider module written for this project, not just default Medusa (default Medusa ships no payment gateway of its own). It’s also one of the more independently-verified parts of the system — see “Verified” below, this isn’t just read-the-code confidence.
Payment status values
Already documented in full in
[Section 8 — Payment status values]/08-order-management/#payment-status-values (not_paid,
authorized, captured, refunded, etc.) — that table is the single source of truth for what
each status means and who changes it. This section covers the Razorpay-specific mechanics
behind those statuses instead of repeating them.
How payment status relates to order/fulfillment status
These are genuinely independent — see the correction already made in [Section 8]/08-order-management/#correcting-a-assumption-from-the-original-brief. A realistic combination, in the order they’d normally occur:
| Payment status | Fulfillment status | What it means in practice |
|---|---|---|
authorized | not_fulfilled | Customer paid, order is new — normal starting point |
captured | not_fulfilled | Funds taken (or auto-captured), still needs packing |
captured | fulfilled → shipped → delivered | Normal path to completion |
requires_action | not_fulfilled | Something needs manual follow-up before proceeding — see [Section 9, step 3]/09-order-processing-sop/#3-verify-payment |
refunded/partially_refunded | any | Money returned — see [Section 15]/15-refunds-and-returns/ |
Capture timing
TO BE CONFIRMED — Medusa’s default applies, no override was found. No automatic-capture setting was found configured anywhere in this project’s seed script or Medusa config, which means Medusa’s standard manual capture behavior applies: a successful checkout leaves the payment as
authorized, and a staff member must click Capture (see [Section 8]/08-order-management/#payment-status-values) to actually take the funds. If the business wants automatic capture instead, that would be a real configuration change, not something already decided.
The real security model (verified, not just read from code)
- The storefront’s Razorpay Checkout.js widget callback is never trusted as proof of
payment. When a cart is completed,
authorizePaymentindependently re-queries Razorpay’s own API for the stored order ID — the only way an order gets created. - Actually tested, not just reasoned about: a cart was taken through address → shipping →
payment session creation, with no real payment made, and
POST /store/carts/:id/completewas confirmed rejected (400 not_allowed, “Session … was not authorized with the provider”), with a direct database query confirming zero orders existed for that cart. - All money math for Razorpay is integer-paise (never floating point), isolated to one
utility (
toPaise/fromPaise) — matching the project’s general money-arithmetic rule (see [Section 16]/16-invoicing/). - Webhook signature verification (HMAC-SHA256, constant-time comparison) was also verified with
synthetic valid/tampered/malformed payloads, and hard-fails (rejects) if
RAZORPAY_WEBHOOK_SECRETisn’t configured, rather than silently skipping verification.
Current environment
- Test mode only — credentials are
rzp_test_..., real money never moves today - Both
pp_razorpay_razorpay(real Razorpay) andpp_system_default(Medusa’s manual provider, kept for local test convenience) are offered in the India region — don’t be surprised to see a second payment option in a local/test environment - Live keys are not wired up — going live would require replacing test credentials, and should not happen before GST invoicing’s placeholder GSTIN is also resolved (see [Section 16]/16-invoicing/)
Known, accepted gaps (not silently missing — documented on purpose)
- Live webhook delivery is untested — no public URL/tunnel exists in local dev, and
RAZORPAY_WEBHOOK_SECRETis blank locally. Not a correctness gap:authorizePaymentindependently re-verifies every checkout regardless of webhooks — webhooks only add out-of-band updates (e.g. a UPI payment that settles after the customer already left the page). Before going live, setRAZORPAY_WEBHOOK_SECRETand register the webhook URL (.../hooks/payment/razorpay_razorpay) in Razorpay’s dashboard. - A known, unrelated bug: the storefront’s product listing page can show a stale cached price after a live price change (product detail pages are correct) — a Next.js caching quirk, not a payments issue, not yet fixed.
Troubleshooting: “payment successful but order pending”
Per the original brief’s instruction not to assume a specific provider’s failure mode — the real, generic investigation path in this system is:
- Check the order’s Payment status badge (see [Section 8]/08-order-management/#payment-status-values)
—
requires_actionmeans Razorpay itself flagged something unresolved. - Use Check Status on the order’s Payment section to force a re-poll against Razorpay’s API
— this is the same re-verification
authorizePaymentdoes automatically at checkout, run again on demand. - If still stuck, this needs a developer — there is no admin-side way to manually force a payment status without going through Razorpay’s own API.
[← Shipping]/13-shipping/ · [Index]/00-index/