Skip to content

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 statusFulfillment statusWhat it means in practice
authorizednot_fulfilledCustomer paid, order is new — normal starting point
capturednot_fulfilledFunds taken (or auto-captured), still needs packing
capturedfulfilledshippeddeliveredNormal path to completion
requires_actionnot_fulfilledSomething needs manual follow-up before proceeding — see [Section 9, step 3]/09-order-processing-sop/#3-verify-payment
refunded/partially_refundedanyMoney 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, authorizePayment independently 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/complete was 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_SECRET isn’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) and pp_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_SECRET is blank locally. Not a correctness gap: authorizePayment independently 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, set RAZORPAY_WEBHOOK_SECRET and 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:

  1. Check the order’s Payment status badge (see [Section 8]/08-order-management/#payment-status-values) — requires_action means Razorpay itself flagged something unresolved.
  2. Use Check Status on the order’s Payment section to force a re-poll against Razorpay’s API — this is the same re-verification authorizePayment does automatically at checkout, run again on demand.
  3. 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/