Happy House - Ecommerce Docs
Developer ResourcesPOS Module Overview

POS Features and Flows

Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the POS module.

POS - Features and Flows

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
Technical design.omc/plans/POS/master-plan.md, consumer-handoff.mdState model, validation, timing, the frozen contract
APIpos-sale-admin.controller.ts, pos-lookup-admin.controller.tsRoute surface, permissions, rate limits
Backendpos-sale-draft.service.ts, pos-sale-completion.service.ts, pos-sale-line.service.ts, pos-customer.service.ts, pos-lookup.service.tsBusiness behavior and side effects
Schemapackages/db/src/schema/pos/*.tsTables, enums, constraints, the state machine as a CHECK

2. Feature Summary

FieldValue
Modulepos
Submodulesale (admin), lookup (admin)
Primary user valueA shop rings up a walk-in sale at the counter and produces an ordinary order with one request
Actorsadmin (the operator), system (the draft sweep)
Main entry points13 admin routes under /api/admin/pos/; the hourly draft sweep
Main outputsA draft sale, a completed picked_up/ordered sale, an order, a payment_attempt, a receipt email, a walk-in invitation email
Related docsBackend, API

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
GuestNothingTouch any routeThere is no customer-facing POS route
Admin (operator)Open a draft, attach/create a customer, scan lines, choose fulfilment, complete, cancelRe-price a line, modify a completed saleJwtAuthGuard + RoleGuard + Pos_*Every mutation is audited with the acting administrator
Admin (reader)List, filter, view one sale, view the timeline, look up customers/productsMutate anythingPos_READLookup is Pos_READ, grantable separately from Users_READ
System (sweep)Cancel drafts idle past 24hTouch a sale being edited, complete a saleQueue/internalOne transaction per draft, guarded UPDATE

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState ReadState WrittenLinked API Section
Find a customerAdminOperatorGET /api/admin/pos/lookup/customerscustomers§8.1
Find a sellable productAdminOperatorGET /api/admin/pos/lookup/productsproducts§8.2
Open a draftAdminOperatorPOST /api/admin/pos/salescustomerspos_sale (draft, pickup), event created§8.3
List salesAdminOperatorGET /api/admin/pos/salespos_sale + joins§8.4
Read one saleAdminOperatorGET /api/admin/pos/sales/{publicId}pos_sale, pos_sale_item§8.5
Read the timelineAdminOperatorGET /api/admin/pos/sales/{publicId}/timelinepos_sale_event§8.6
Attach a customerAdminOperatorPATCH /sales/{publicId}/customercustomerspos_sale (customer + reset to pickup), event customer_assigned§8.7
Create a walk-inAdminOperatorPOST /sales/{publicId}/customercustomers, account (no password), outbox invite, event customer_created§8.8
Set a line quantityAdminOperatorPUT /sales/{publicId}/items/{productPublicId}productspos_sale_item (upsert), pos_sale totals, event item_added/item_quantity_changed§8.9
Remove a lineAdminOperatorDELETE /sales/{publicId}/items/{productPublicId}pos_sale_item (hard delete), totals, event item_removed§8.10
Choose fulfilmentAdminOperatorPATCH /sales/{publicId}/fulfilmentcustomer_addressespos_sale, customer_addresses (new address), event fulfilment_set§8.11
Complete the saleAdminOperatorPOST /sales/{publicId}/completepos_sale, lines, products (prices), inventorycart, session, payment_attempt, order, reservation finalize, pos_sale terminal, outbox receipt, events§8.12
Cancel a draftAdminOperatorPOST /sales/{publicId}/cancelpos_sale (cancelled), event cancelled (reason required)§8.13
Sweep abandoned draftsSystemSweephourly cron → pos.sweep_abandoned_draftspos_salepos_sale (cancelled), event cancelled§9

5. User-Facing Flows

There is no customer-facing flow in this module. The walk-in customer is present at the counter but every action is performed by the administrator on their behalf; the customer-facing "flows" below are what the customer experiences through the operator.

5.1 A walk-in buys over the counter

Summary

A customer walks in without an account. The operator finds or creates the customer, scans the items, decides pickup or delivery, takes the money and hands the goods over — one request at the end does the money, the stock and the order together.

Preconditions

  • The operator holds Pos_CREATE/Pos_UPDATE/Pos_READ and the till has connectivity.
  • The customer either already has an account, or supplies a name and email.

Main Flow

StepActor/SystemActionResultSource
1OperatorLook up the customer by name/email/phoneMatch listpos-lookup.service.ts
2OperatorOpen the draft with payment method + customerpos_sale status draft, fulfilment pickuppos-sale-draft.service.ts
3Operator(New customer) create the walk-inAccount + email invitation queuedpos-customer.service.ts
4OperatorScan each item, typing the final quantityLine frozen with the price, totals recomputedpos-sale-line.service.ts
5Operator(Delivery only) choose fulfilment + addressdelivery with address and shipping chargepos-sale-draft.service.ts
6OperatorCompleteMoney, stock, order, hand-over in one transactionpos-sale-completion.service.ts
7SystemReceipt emailReceipt queued via outbox → NOTIFICATIONSpos-email.processor.ts

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
No matching customerSearch too short400 VALIDATION_FAILEDType ≥ 2 characters
Customer has no accountUnknown walk-inCreate walk-in firstcustomerId required at open
Email already takenExisting customer409 POS_CUSTOMER_EMAIL_TAKENAttach the existing customer instead
Same item scanned twiceSecond PUTQuantity REPLACED, not addedLine ends at the typed number
Price moved mid-salesellingPrice != frozen unitPrice409 POS_PRODUCT_PRICE_CHANGEDRemove and re-add; nothing charged
Someone else took the stockReserve fails409 POS_INSUFFICIENT_STOCKNothing charged, sale still a draft
Completion replayedSame request twiceSecond attempt collides on unique order/cart/session keys409 POS_SALE_ALREADY_COMPLETED, no second charge

5.2 The customer's set-password journey

Summary

The walk-in account has no password. The customer receives an emailed link to set one. The link never expires but is single-use — it stops working the moment a password is successfully set.

Preconditions

  • The operator created the walk-in (which queued the invitation in the same transaction).

Main Flow

StepActor/SystemActionResultSource
1WorkerRuns pos.send_walk_in_inviteMints token, renders email, queues to NOTIFICATIONSpos-email.processor.ts
2CustomerOpens the emailed linkLand on /set-password?token=...pos-walk-in-invite.email.ts
3CustomerSets a passwordverification.consumed_at stamped; token refused from then onVerificationTokenService

Branches and Edge Cases

BranchConditionBehaviorError/Result
Customer deleted the accountBetween sale and jobWorker skips (outcome: "skip")No retry of an invitation to a closed account
No email on fileCustomer without emailWorker skipsNothing to send to
Link used twiceAfter first setToken consumedRefused; customer logs in normally
Link never usedWeeks laterStill valid — never expiresBy owner policy; bounded by single use

6. Admin Flows

6.1 The full till flow (activity)

6.2 Abandoning a draft

StepActionResult
1POST /sales/{publicId}/cancel with a reasonpos_salecancelled, event cancelled with mandatory reason
2Operator opens a new draftFresh POS-2026-... number

A draft may also be cancelled by the hourly sweep after 24h idle. Both paths record cancelled; only the operator's carries actor_admin_id (the sweep writes null).

7. Lifecycle and State Transitions

EntityFromEvent/ActionToGuard ConditionSide Effects
pos_saledraftComplete (pickup)picked_upLines present, customer set, fulfilment pickupStock, payment, order, hand-over
pos_saledraftComplete (delivery)orderedLines present, customer set, address setStock, payment, order
pos_saledraftCancel (operator)cancelledReason non-blankEvent with actor + reason
pos_saledraftSweep (24h idle)cancelledupdated_at <= now() - 24h, guarded WHERE status='draft'Event with sweep reason, no actor

There is no awaiting_payment. An online checkout needs that state because the customer leaves for a gateway and comes back; a counter sale has no such gap — the operator takes the money and records it in the same request. Modelling a wait that cannot happen would create a state every query has to handle and no code path can ever produce.

picked_up is terminal for POS but the order is already delivered; ordered is terminal for POS but the order is at confirmed and the Order module owns every state after it.

9. Data and Side Effects by Flow

FlowDB WritesCache EffectsJobsRealtimeAnalyticsNotifications
Open draftpos_sale + event
Create walk-incustomers, account, eventoutbox → pos.send_walk_in_inviteSet-password email
Line upsert/removepos_sale_item (+hard delete), totals, event
Set fulfilmentpos_sale, maybe customer_addresses, event
Completepos_sale terminal, cart, session, payment_attempt, order, reservation finalize, eventsinventory domain after commitoutbox → pos.send_receiptReceipt email
Cancelpos_sale + event
Sweeppos_sale + event per draftdirect enqueue (cron exemption)

10. Error and Recovery Flows

ScenarioTriggerUser/System ExperienceRecoverySource
Rate limited> 60 draft writes/min or > 30 completes/min429Wait; the limits are far above genuine till useip-throttler.config.ts
Price changedCatalogue moved mid-sale409 POS_PRODUCT_PRICE_CHANGEDRemove and re-add the line; nothing was chargedpos-sale-completion.service.ts
Stock shortTwo tills, one unit409 POS_INSUFFICIENT_STOCKShow remaining stock, reduce the quantitysame
Completion replayRetried after commit409 POS_SALE_ALREADY_COMPLETEDShow the existing order number; do not retryunique indexes
Queue outageRedis/BullMQ downSale still commits; receipt/invite delayedOutbox row is durable; dispatcher relays laterpos-mail-queue.service.ts
Deterministic job failuree.g. sale not foundJob returns { success: false, retryable: false, errorCode }Never retried; visible in BullMQ return valuepos-queue.processor.ts
Transient job failureDB connection dropRethrown for BullMQ retryAutomaticsame

12. Mandatory Feature and Flow Deep-Dive Pack

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorActorTriggerUser/System ResultBackend Side EffectSource
Customer lookupSearch min 2 charsOperatorGET /lookup/customersBounded, paginated matchesILIKE on name/email/phone, active customers onlypos-lookup.service.ts
Product lookupSKU or nameOperatorGET /lookup/productsBounded, paginated matchespublished + not deleted + price ≤ MRP onlysame
Open draftAlways pickupOperatorPOST /salesDraft with fulfilment: "pickup"fulfilment deliberately not accepted in DTOpos-sale-request.dto.ts
Attach customerResets to pickupOperatorPATCH /customerSale drops address/fulfilment/shippingCLEAR_DELIVERY_ON_CUSTOMER_CHANGEpos-sale-draft.service.ts
Walk-in createEmail lowercasedOperatorPOST /customerAccount + invitationNo password; email_verified: falsepos-customer.service.ts
Line upsertReplaces quantityOperatorPUT /items/{id}Line at typed quantityON CONFLICT DO UPDATEpos-sale-line.service.ts
Line removeHard deleteOperatorDELETE /items/{id}Line gone; totals recomputedHard delete — the sale of record is order_itemsame
New addressSaved to customer bookOperatorPATCH /fulfilmentAddress reusable lateris_default: false deliberatelypos-sale-draft.service.ts
Payment referenceRecorded, never validatedOperatorPOST /completeReference on the receiptRefused beside pos_cashpos-sale-completion.service.ts
ReceiptSkipped without emailSystempos.send_receiptNo email, job logged skipoutcome: "skip"pos-email.processor.ts
InviteSkipped for deleted accountSystempos.send_walk_in_inviteNo email, job logged skipoutcome: "skip"same
SweepBatch capSystemhourly cronStops at 100Warns when the batch fillspos-draft-sweep.processor.ts

12.2 Business Process Diagram Pack

The full till flow diagram (§6.1), the sale lifecycle (§7), the completion sequence (§5.1) and the queue topology below cover the module's business processes.

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonActor ImpactEnforced InAPI ImpactBackend ImpactTests
A sale always opens as a pickupopen() cannot know an address yet; delivery needs oneOperator re-chooses deliveryDTO omits the fieldNo fulfilment on POST /salesSchema CHECKpos-sale.int.spec.ts
One live review needs a delivered order— (POS enables it)Pickup drives order to deliveredorder-counter-handover.service.ts
Every completed sale has every downstream keyNo paid sale without an orderOperator sees orderNumberchk_pos_sale_status_completionconstraint probe
A cancelled sale needs a non-blank reasonAudit answers whyOperator must type a reasonDTO + chk_pos_sale_event_cancel_reasonMandatory body fieldprobe
Cash has no referenceA reference beside cash is a mis-selected methodOperator clears the fieldDTO + chk_pos_sale_payment_reference_method409probe
A pickup charges no shippingMoney for a service not renderedOperator sees shipping 0chk_pos_sale_fulfilment_addressprobe
The walk-in has no passwordCredentials are the customer's aloneCustomer sets own passwordPosCustomerServiceinvitationQueued only, never a tokenpassword: nullpos-customer path
A draft holds no inventoryReserves only at completionPOS_DRAFT_ABANDON_HOURS semanticsSweep is housekeeping

12.4 Tradeoffs and Product Rationale

Product DecisionUser BenefitEngineering BenefitAlternativeTradeoffRisk
POS reuses the online pipelineOne order domain, one return/refund pathNo second ledgerA parallel counter-only recordMaterializer complexityChannel is a label, never a branch
Drafts are separate rows, not cartsA phone cart and a till sale coexistNo collision with uq_cart_customer_id_liveReusing cart_itemCart born converted is unusualDocumented on the schema
No awaiting_paymentOperator never sees a stuck stateOne less state everywhereModelling the wait
Never-expiring single-use linkA walk-in can always get inNo support backlog24h TTLLong-lived link if email is stolenSingle use; no OTP
Price freeze + loud refusalThe quoted price is the price paidNo silent re-pricingCharge stale priceMid-sale 409sOperator + customer are both present
Hard-deleted draft linesSimplerSoft deleteDraft history lostorder_item is the record of sale

12.5 Flow Edge-Case Matrix

FlowEdge CaseTriggerExpected BehaviorUser/System FeedbackSource
OpenCustomer soft-deletedAttach a closed account404 POS_SALE_NO_CUSTOMEROperator looks elsewherepos-sale-draft.service.ts
Walk-inEmail of a soft-deleted accountCreate walk-in409 POS_CUSTOMER_EMAIL_TAKEN (closed-account message)Use a different addresspos-customer.service.ts
FulfilmentBoth addressPublicId and addressSend both409 POS_DELIVERY_ADDRESS_REQUIREDPick onepos-sale-draft.service.ts
FulfilmentAddress not this customer'sSend another's addressPublicId404 POS_ADDRESS_NOT_FOUNDRe-fetch their addressessame
LineQuantity 0 or 100PUT with bad number400 POS_QUANTITY_OUT_OF_RANGEClamp in UIpos-sale-line.service.ts
LineProduct unpublished mid-salePUT a delisted product404/409 POS_PRODUCT_NOT_SELLABLERemove the linesame
CompleteEmpty basketComplete with no lines409 POS_SALE_EMPTYBlock until a line existscompletion service
CompleteDelivery with no addressComplete delivery draft409 POS_DELIVERY_ADDRESS_REQUIREDOpen the address formsame
CompleteSale already terminalReplay completion409 POS_SALE_ALREADY_COMPLETEDShow existing order numberunique indexes
SweepOperator resumes a claimed draftRace between sweep and operatorGuarded UPDATE cancels nothing; operator winsDraft stays livepos-draft-sweep.processor.ts

12.6 Flow-to-Data Trace

FlowReadsWritesCacheJobs/EventsResponse Fields
Opencustomerspos_sale, eventPosSaleDto (empty items)
Walk-incustomers, account, event, outboxpos.send_walk_in_invitesale, customerId, invitationQueued
Line upsertproductspos_sale_item, totals, eventPosSaleDto
Completesale, lines, products, inventorycart, session, payment_attempt, order, reservation, pos_sale, events, outboxinventory (after commit)pos.send_receiptsale, orderNumber, orderPublicId
Timelinepos_sale_eventevents[]

12.7 Experience Quality Checklist

  • The doc explains what the operator is trying to accomplish (a walk-in sale end to end).
  • The doc explains what the backend does the actor does not see (one transaction, unique-key replay protection, outbox emails).
  • Every minor flow and branch is covered (lookup minimum, reset-on-customer-change, skip outcomes, batch caps).
  • Admin and system flows are included; there is no user flow by design.
  • Business logic, tradeoffs and rationale are explained (§12.3, §12.4).
  • Every flow maps to API routes and backend side effects (§4, §9, §12.6).
  • Diagrams fit each flow type (sequence, activity, state, queue topology).
  • Edge cases and failure recovery are covered (§5.1, §10, §12.5).

13. Completion Checklist

  • Every feature, minor action, and submodule capability is listed (§4, §12.1).
  • Every actor has allowed and forbidden behavior (§3).
  • Every major and minor flow includes steps, branches, and diagrams (§5, §6, §12.2).
  • Every lifecycle has a transition table and state diagram (§7).
  • Every flow links to the API and backend docs (§4, §12.6).
  • TDD dependencies are called out where they shape behavior (constraint probe, §12.3).

See Also

  • API doc: /docs/developer/pos/api
  • Backend doc: /docs/developer/pos/backend
  • TDD: not yet published