Payment Module Overview
Taking money for a frozen checkout — eSewa and COD, attempt-scoped transitions, and reconciliation as a flag.
Audience: Product owners, QA, frontend and backend developers Scope: The payment surface, the attempt model, and the reconciliation flag
Payment Module - Overview
1. What the module is
Payment takes money for a frozen checkout, via eSewa and Cash on Delivery. Four customer routes, four public gateway-return routes (eSewa's, not yours), three admin routes.
A customer picks a method, the API creates a payment attempt, and either the payment settles immediately (COD) or the client hands the customer to eSewa with a signed form. eSewa returns the browser to the API, which verifies the payment server-to-server and then redirects to the storefront's result page with the outcome in the query string.
The storefront never handles gateway fields, never verifies anything, and never learns eSewa's vocabulary. It POSTs a form it is given, and reads one of seven business statuses.
The checkout, cart, promotion, inventory and shipping modules were not modified — zero files. Payment consumes checkout's payment contract, which checkout published with no callers.
2. The routes
| Surface | Route | Permission |
|---|---|---|
| Customer | GET /api/mobile/payments/methods | — |
| Customer | POST /api/mobile/payments | — |
| Customer | GET /api/mobile/payments/:id | — |
| Customer | POST /api/mobile/payments/:id/cancel | — |
| Gateway | GET/POST /api/payments/esewa/return/:attemptId/:token/success-or-failure | Public, eSewa only |
| Admin | GET /api/payments | Payments_READ |
| Admin | GET /api/payments/:id | Payments_READ |
| Admin | POST /api/payments/:id/resolve | Payments_UPDATE |
Do not call the gateway-return routes. They exist for eSewa's redirect and carry a per-attempt secret.
3. The rules that matter
- There is no
paymenttable.checkout_sessionalready holds the frozen money, the customer, the lifecycle and the attempt counter — a parallelpayment.amountwould be a second copy ofgrand_totalkept equal only by application code remembering to. Payment ownspayment_attempt(one row per interaction with a method) andpayment_eventbeneath it. "One checkout, one successful payment" is a partial unique index, not a service-layer argument about concurrency. - A gateway's answer carries an AMOUNT, never a boolean. The confirmed variant holds the figure and the currency, so a caller cannot reach "this succeeded" without holding them — and a CHECK refuses to record a confirmed amount that differs from the one requested. That combination makes the replay defect this module was built against structurally unwritable.
- Payment runs in several short transactions, not one. Checkout could be a single transaction because every collaborator was a module over the same database; a gateway is over HTTP, and a network call cannot sit inside a transaction holding inventory row locks.
- The attempt is marked
succeededBEFORE the checkout completes, and the same transaction enqueues the completion through the outbox — a shipped order can never exist without a money record, and the outbox row makes the window survivable rather than merely observable. - Reconciliation is a FLAG, not a status. "A human must look at this" is a fact ABOUT an outcome, not an outcome. As a status it would have erased
succeeded_aton the one row where money provably moved. - COD is a payment method, not a bypass — same attempt row, same transitions, same audit trail. Its "gateway" approves at hand-off.
- Payment never writes
checkout_session. Doing so would bypass inventory finalisation and promotion confirmation — the customer is charged, stock never decremented, and nothing errors.
Page guide
| Page | Read it for |
|---|---|
| Features and flows | The seven business statuses, the result-page flow, error-recovery |
| Backend | The attempt model, the port, short transactions, the reconciliation flag |
| API | All nine endpoints, DTOs and error codes |