Happy House - Ecommerce Docs
Developer ResourcesPayment

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

SurfaceRoutePermission
CustomerGET /api/mobile/payments/methods
CustomerPOST /api/mobile/payments
CustomerGET /api/mobile/payments/:id
CustomerPOST /api/mobile/payments/:id/cancel
GatewayGET/POST /api/payments/esewa/return/:attemptId/:token/success-or-failurePublic, eSewa only
AdminGET /api/paymentsPayments_READ
AdminGET /api/payments/:idPayments_READ
AdminPOST /api/payments/:id/resolvePayments_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

  1. There is no payment table. checkout_session already holds the frozen money, the customer, the lifecycle and the attempt counter — a parallel payment.amount would be a second copy of grand_total kept equal only by application code remembering to. Payment owns payment_attempt (one row per interaction with a method) and payment_event beneath it. "One checkout, one successful payment" is a partial unique index, not a service-layer argument about concurrency.
  2. 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.
  3. 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.
  4. The attempt is marked succeeded BEFORE 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.
  5. 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_at on the one row where money provably moved.
  6. COD is a payment method, not a bypass — same attempt row, same transitions, same audit trail. Its "gateway" approves at hand-off.
  7. 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

PageRead it for
Features and flowsThe seven business statuses, the result-page flow, error-recovery
BackendThe attempt model, the port, short transactions, the reconciliation flag
APIAll nine endpoints, DTOs and error codes