Happy House - Ecommerce Docs
Developer ResourcesCheckout

Checkout Module Overview

Turning a cart into a validated, price-frozen purchase attempt that holds stock — one transaction, no saga.

Audience: Product owners, QA, frontend and backend developers Scope: The checkout surface, the one-transaction model, and the payment contract the Payment module inherits

Checkout Module - Overview

1. What the module is

Checkout turns a cart into a validated, price-frozen purchase attempt that holds stock. Three customer routes and four admin routes. Nothing here takes money — payment is a separate module that does not exist yet.

A customer can: start checkout (POST /api/mobile/checkout), fetch their live session (GET /api/mobile/checkout/active), and cancel it (POST /:id/cancel). Support can: list sessions, read one, and release a session by cancel or force-expire.

The cart, promotion, inventory and shipping modules were not modified — zero files. Checkout consumes their contracts.

2. The routes

SurfaceRoutePermission
CustomerPOST /api/mobile/checkout
CustomerGET /api/mobile/checkout/active
CustomerPOST /api/mobile/checkout/:id/cancel
AdminGET /api/checkout/sessionsCheckout_READ
AdminGET /api/checkout/sessions/:idCheckout_READ
AdminPOST /api/checkout/sessions/:id/cancelCheckout_UPDATE
AdminPOST /api/checkout/sessions/:id/expireCheckout_UPDATE

{id} is always a uuid7 public_id; no integer primary key is exposed.

3. The rules that matter

  1. It is ONE database transaction, not a saga. Cart, inventory, promotion, shipping and address are modules over the same PostgreSQL database, and every write path accepts a DbExecutor — so validation, both reservations and the freeze commit together or not at all. A failed checkout leaves nothing behind: no orphan hold, no locked cart. This is why the module is small.
  2. The holds outlive the session on purpose. Inventory and promotion each sweep their own holds on their own schedule, and neither has heard of a checkout session — a hold reserved for only as long as the session lives would be reclaimed while the customer was still at the payment gateway.
  3. Expiry is a read predicate, never a stored status. A session past its expires_at is expired to every reader whether or not the sweep has run.
  4. payment_in_progress is exempt from the sweep — releasing stock under a customer at a gateway turns a successful charge into an unfulfillable order. The cost: a gateway that never calls back leaves a session only an administrator can clear, which is what the admin force-expire route is for.
  5. A declined card does not destroy the basket. A retryable failure returns the session to pending_payment with the holds intact.
  6. Every payment-side transition is attempt-scoped, not merely idempotent — the retry makes the state machine cyclic.
  7. Everything on a session is a snapshot. Product name, SKU, price, MRP, the promotion's name and what it was worth, the whole shipping address. A receipt must never join product or promotion — an operator editing one would otherwise rewrite the history of every past purchase.

Page guide

PageRead it for
Features and flowsActor journeys, the session lifecycle, error-recovery flows, edge cases
BackendThe one-transaction model, holds/TTL, expiry-as-predicate, the sweep exemption, the totals invariant
APIAll seven endpoints, DTOs and error codes