Happy House - Ecommerce Docs
Developer Resources

POS Module Overview

The admin-only counter till — draft sales, walk-in customers, pickup or delivery, and completion that takes money, moves stock and creates an ordinary order in one transaction.

Audience: Product owners, QA, frontend and backend developers Scope: The counter till, walk-in accounts, completion semantics, and how a counter sale becomes an ordinary order

POS Module - Overview

1. What the module is

POS is the counter till: an administrator rings up a walk-in sale — find or create the customer, scan products, choose pickup or delivery, take cash, card or QR, and complete. The result is an ordinary order in the ordinary order lifecycle, distinguishable from an online one only by the channel it records.

There is no customer-facing POS route, and there will not be one. A walk-in customer never touches this API. Every route is an administrator acting on their behalf — that is the one thing that distinguishes a counter sale from an online one.

2. The routes — 13, all admin

SurfaceRoutePermission
LookupGET /api/admin/pos/lookup/customers · /lookup/productsPos_READ
DraftPOST /api/admin/pos/sales · GET /sales · GET /sales/{publicId} · GET /sales/{publicId}/timelinePos_CREATE / Pos_READ
DraftPATCH /sales/{publicId}/customer · POST /sales/{publicId}/customerPos_UPDATE / Pos_CREATE
LinesPUT /sales/{publicId}/items/{productPublicId} · DELETE /sales/{publicId}/items/{productPublicId}Pos_UPDATE
FulfilmentPATCH /sales/{publicId}/fulfilmentPos_UPDATE
TerminalPOST /sales/{publicId}/complete · POST /sales/{publicId}/cancelPos_CREATE / Pos_DELETE

The permission module is spelled PosPos_CREATE, Pos_READ, Pos_UPDATE, Pos_DELETE. Not POS. A role must be granted these before an operator can use the till.

3. The one idea that shapes everything: POS is not a second kind of purchase

A counter sale drives the same cart → checkout_session → payment_attempt → order pipeline a customer drives from their phone. It does not copy the commercial facts anywhere: money and lines live on checkout_session_item and then order_item, stock moves through inventory_reservation, the payment is a payment_attempt.

pos_sale is the orchestration record — it adds the four things that pipeline genuinely cannot answer, because online purchases have no such concept:

  1. Which administrator made the sale (created_by_admin_id, RESTRICT — an offboarded operator whose sales became unattributed would silently rewrite last quarter's numbers).
  2. How the goods leave the shop (fulfilment — the discriminator behind every branch).
  3. Whether the sale created the customer account (customer_created — stored, because it cannot be reconstructed afterwards).
  4. The draft — a state the online pipeline has no equivalent of.

The downstream keys (cart_id, checkout_session_id, payment_attempt_id, order_id) are nullable while drafting, NOT NULL once completed, each under its own partial unique index — which is what makes a replayed completion a no-op rather than a second charge.

4. Completion is ONE transaction

Stock moves, the payment is recorded, the order is created and — for a pickup — the goods are handed over, together or not at all. There is no state in which a customer has paid and no order exists, which is the state that cannot be cleaned up afterwards.

The lock order matters: pos_sale → products ASCENDING → cart → checkout_session → payment_attempt → order. Ascending product id is what online checkout already does, so a counter sale and an online checkout for the same popular item cannot deadlock.

A walk-in customer is created with NO password — deliberately, and by explicit owner decision. The customer receives an emailed set-password link that never expires but is single-use: verification.consumed_at is stamped the moment a password is set, and a consumed token is refused from then on.

Never suggest a password is emailed. Nothing usable is ever emailed, and no administrator ever holds a credential that would still work tomorrow. The invitation deliberately mints no OTP — a six-digit code that never expires is a brute-forcible credential; the long random token is not.

6. Pickup and delivery

FulfilmentAddressShippingTerminal sale statusOrder
pickupnone — the store's own address is stamped0picked_updriven straight to delivered via a synthetic Counter shipment — which starts the return window and makes the purchase reviewable
deliveryrequired — saved to the customer's address bookoperator-setorderedleft at confirmed for the normal lifecycle

Changing the customer resets the sale to a pickup: the saved address belonged to one person, so the address, the fulfilment and the shipping charge are cleared together.

7. Money

All money is integer minor units. 500000 is NPR 5,000.00. Never parse it as a float and never do arithmetic on a formatted string. The sale total is recomputed from the persisted lines on every mutation — never supplied by a client — and chk_pos_sale_grand_total_matches refuses to store a total that does not follow from its parts.

8. What the module deliberately does not do

  • No own inventory, order or payment logic. It orchestrates the existing modules.
  • No barcode search. product has sku but no barcode column, so barcode search was not built. A scanner that emits the SKU works against product lookup today.
  • No refund or return path. A completed sale's correction is an order cancellation or return, owned by the order module.
  • Drafts hold no stock. POS reserves only at completion — the abandoned-draft sweep is housekeeping, not a release path.

9. Where to go next

PageFor
Features and flowsThe till flow end to end, the state machine, and the edge-case matrix
BackendSchema, services, the one-transaction completion, queues and operations
API referenceAll 13 endpoints with request and response shapes and error codes