Happy House - Ecommerce Docs
Developer ResourcesPOS Module Overview

POS API Reference

Complete API contracts for the POS module, including routes, auth, DTOs, responses, errors, examples, and integration notes.

POS - API Reference

Audience: Frontend engineers (admin panel), backend engineers, QA, and API consumers. Scope: The 13 admin-only counter routes under /api/admin/pos/ — sale drafting, completion, cancellation, and customer/product lookup.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllersapps/api/src/modules/pos/admin/sale/pos-sale-admin.controller.ts, admin/lookup/pos-lookup-admin.controller.tsRoutes, methods, guards, permissions, rate limits
DTOsadmin/sale/dto/*.ts, admin/lookup/dto/pos-lookup.dto.tsRequest, query, response, validation
Servicesshared/*.service.tsBehavior, transactions, error codes
Schemapackages/db/src/schema/pos/*.tsTables, enums, constraints
Jobs/cachepackages/jobs/src/index.ts, cache-invalidation.tags.tsOutbox jobs, inventory domain
Existing docsconsumer-handoff.md, Fumadocs formatsFrozen contract and format baseline

2. Module Summary

FieldValue
Module namepos
Module slugpos
Primary actorsadmin (operator), system (sweep)
API surfacesadmin only — no customer-facing route exists
Base route prefixes/api/admin/pos/sales, /api/admin/pos/lookup
Auth modelJwtAuthGuard + RoleGuard + Pos_* permissions
PersistencePostgreSQL (pos_sale, pos_sale_item, pos_sale_event + the online pipeline rows)
Runtime source of truthpos_sale row; the order is the Order module's
Sibling docsBackend, Features and flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
saleNumberPOS-2026-000042, the till-roll reference printed on the receiptpos-sale-number.service.tsEvery response
statusdraftpicked_up/ordered or cancelled; no awaiting_paymentenums.tsEvery response
fulfilmentpickup/delivery — the discriminator behind every branchenums.tsFulfilment + complete
customerCreated§48 Customer Type — TRUE when this sale created the accountpos_sale.customer_createdSale response
paymentReferenceThe terminal/QR slip number; recorded, never validatedpos_sale.payment_referenceComplete
channel"pos" on the order/session; a label, never a branchsales_channel enumRead-only
invitationQueuedThe walk-in invite is queued; the token is never returnedPosWalkInCreatedDtoWalk-in response
orderNumberHS-2026-000042, printed on the receiptOrder moduleCompletion response

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
AdminGET/api/admin/pos/lookup/customersOperatorJWT + RolePos_READPosLookupAdminControllerFind a customer by name/email/phone
AdminGET/api/admin/pos/lookup/productsOperatorJWT + RolePos_READPosLookupAdminControllerFind a sellable product by name/SKU
AdminPOST/api/admin/pos/salesOperatorJWT + RolePos_CREATEPosSaleAdminControllerOpen a draft (always pickup)
AdminGET/api/admin/pos/salesOperatorJWT + RolePos_READPosSaleAdminControllerList and filter sales
AdminGET/api/admin/pos/sales/{publicId}OperatorJWT + RolePos_READPosSaleAdminControllerOne sale with its lines
AdminGET/api/admin/pos/sales/{publicId}/timelineOperatorJWT + RolePos_READPosSaleAdminControllerThe audit trail
AdminPATCH/api/admin/pos/sales/{publicId}/customerOperatorJWT + RolePos_UPDATEPosSaleAdminControllerAttach an existing customer
AdminPOST/api/admin/pos/sales/{publicId}/customerOperatorJWT + RolePos_CREATEPosSaleAdminControllerCreate a walk-in customer
AdminPUT/api/admin/pos/sales/{publicId}/items/{productPublicId}OperatorJWT + RolePos_UPDATEPosSaleAdminControllerSet a line's FINAL quantity
AdminDELETE/api/admin/pos/sales/{publicId}/items/{productPublicId}OperatorJWT + RolePos_UPDATEPosSaleAdminControllerRemove a line
AdminPATCH/api/admin/pos/sales/{publicId}/fulfilmentOperatorJWT + RolePos_UPDATEPosSaleAdminControllerPickup or delivery (+ address)
AdminPOST/api/admin/pos/sales/{publicId}/completeOperatorJWT + RolePos_CREATEPosSaleAdminControllerTake payment, create the order
AdminPOST/api/admin/pos/sales/{publicId}/cancelOperatorJWT + RolePos_DELETEPosSaleAdminControllerAbandon a draft

5. Auth, Identity, and Permissions

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
AdminJwtAuthGuard, RoleGuardreq.user.idPos_CREATE/Pos_READ/Pos_UPDATE/Pos_DELETENoThe permission module is spelled Pos, never POS; Pos_READ exposes customer lookup, so it is grantable separately from Users_READ
  • ParseUUIDPipe on every :publicId and :productPublicId — a malformed id is a clean 400.
  • Rate limits key on the user, not the IP — every till in a shop shares one public address.

6. DTO and Model Reference

6.1 OpenPosSaleDtoPOST /sales

FieldTypeRequiredValidationExample
paymentMethodstringYes@IsIn(POS_PAYMENT_METHODS)pos_cash/pos_card/pos_qr"pos_cash"
customerIduuidYes@IsUUID018f1e2a-...-000000000001

Deliberately no fulfilment field. Every sale opens as a pickup; delivery is chosen with PATCH /fulfilment, the only endpoint that can take an address in the same request. Sending fulfilment here is rejected by forbidNonWhitelisted.

6.2 AttachPosCustomerDtoPATCH /sales/{publicId}/customer

FieldTypeRequiredValidation
customerIduuidYes@IsUUID

6.3 CreatePosWalkInDtoPOST /sales/{publicId}/customer

FieldTypeRequiredValidationNotes
namestringYes≤255, trimmed
emailstringYes@IsEmail, ≤320, trimmed + lowercasedThe account identifier and invite destination
phonestringNo≤20

6.4 UpsertPosLineDtoPUT /sales/{publicId}/items/{productPublicId}

FieldTypeRequiredValidationNotes
quantitynumberYes@IsInt, 1–99The FINAL quantity, not a delta. Scanning the same product twice sends 2, not two calls of 1

6.5 SetPosFulfilmentDtoPATCH /sales/{publicId}/fulfilment

FieldTypeRequiredValidationNotes
fulfilmentstringYespickup/delivery
addressPublicIduuidNo@IsUUIDMutually exclusive with address
addressobjectNoPosDeliveryAddressDtoMutually exclusive with addressPublicId
shippingAmountnumberNo@IsInt, ≥ 0Minor units; forced 0 for pickup

PosDeliveryAddressDto: districtId (int ≥1), municipalityName (≤120), ward (int 1–40), recipientName (≤120), recipientPhone (E.164 ^\+[1-9][0-9]{7,14}$), street (≤200), landmark (optional ≤200), postalCode (optional, five digits).

6.6 CompletePosSaleDtoPOST /sales/{publicId}/complete

FieldTypeRequiredValidationNotes
paymentReferencestringNo≤128, trimmedPermitted for pos_card/pos_qr only — refused beside pos_cash. Recorded, never validated

6.7 CancelPosSaleDtoPOST /sales/{publicId}/cancel

FieldTypeRequiredValidationNotes
reasonstringYes@IsNotEmpty, ≤500, trimmedMandatory — an audit entry without a why answers nothing

6.8 ListPosSalesQueryDtoGET /sales

FieldTypeDefaultValidationNotes
pagenumber1≥1
sizenumber201–100
statusenumdraft/picked_up/ordered/cancelled
fulfilmentenumpickup/delivery
paymentMethodenumany payment_method§54 tender split
customerIduuid
createdByAdminIduuid§51/§54 sales-by-administrator
fromISO date@IsDateStringInclusive lower bound on created_at
toISO date@IsDateStringInclusive upper bound

6.9 PosLookupQueryDto — both lookup routes

FieldTypeRequiredValidationNotes
searchstringYes≤100, trimmed; ≥2 chars (else 400 VALIDATION_FAILED)Name/email/phone (customers), name/SKU (products)
pagenumberNo≥1
sizenumberNo1–100

6.10 Response DTOs

DTOShapeNotes
PosSaleDtopublicId, saleNumber, status, fulfilment, currency, customer { customerId, name, email, createdByThisSale }, payment { method, reference }, totals { subtotal, discountAmount, shippingAmount, grandTotal, totalQuantity, lineCount }, items[], orderNumber?, orderPublicId?, createdByAdminName?, createdAt, completedAt?, cancelledAt?Grouped by §47 business objects, not table rows; no integer PKs anywhere
PosSaleListItemDtopublicId, saleNumber, status, fulfilment, paymentMethod, grandTotal, totalQuantity, customerName?, createdByAdminName?, orderNumber?, createdAtList row
PosSaleEventDtopublicId, eventType, fromStatus?, toStatus?, actorName? (null after offboarding), reason?, createdAtTimeline row
PosSaleCompletionDtosale, orderNumber (print on the receipt), orderPublicIdCompletion
PosWalkInCreatedDtosale, customerId, invitationQueued: trueNever a token — a credential does not belong in a response body
PosCustomerMatchDtocustomerId, name, email?, phone?Lookup row — no addresses, no history
PosProductMatchDtoproductPublicId, name, sku?, mrp, sellingPriceLookup row

7. Enum Reference

EnumValueMeaningRuntime EffectSource
pos_sale_statusdraftBeing rung up; only editable stateLines may change; cancel is freeenums.ts
pos_sale_statuspicked_upPickup sale, goods handed overTerminal; order already delivered
pos_sale_statusorderedDelivery sale, order at confirmedTerminal for POS; Order owns the rest
pos_sale_statuscancelledAbandoned before completionOnly reachable from draft
pos_fulfilment_typepickupCustomer takes the goodsNo address, no shipping
pos_fulfilment_typedeliveryDelivered to an addressAddress + shipping required
pos_sale_event_typecreated / customer_assigned / customer_created / item_added / item_quantity_changed / item_removed / fulfilment_set / payment_recorded / completed / cancelledWhat happenedTimeline rows
sales_channelonline / posWhere the order came fromLabel only — never a branchcheckout/enums.ts

8. Endpoint Reference

8.1 GET /api/admin/pos/lookup/customers?search=

Purpose

Find a returning customer at the till by name, email or phone. Bounded and paginated: two characters minimum, and it returns identity fields only — never addresses or purchase history, which belong on the customer's own detail endpoint.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard
  • Permission: Pos_READ
  • Rate limit: PUBLIC_SEARCH

Request

PartRequiredDetails
QueryYessearch (≥2 chars), page?, size? (≤100)

Response

200{ items: [{ customerId, name, email?, phone? }] }. Only status = 'active', not soft-deleted, customers match.

Side Effects

None — a read over customers.

Error Cases

HTTPCodeCondition
400VALIDATION_FAILEDsearch shorter than 2 characters

8.2 GET /api/admin/pos/lookup/products?search=

Purpose

Find a sellable product by name or SKU. Only published, non-deleted products with selling_price <= mrp match — a counter must not become the way unpublished stock leaves the building.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_READ · Rate limit: PUBLIC_SEARCH

Request

PartRequiredDetails
QueryYessearch (≥2 chars), page?, size?

Response

200{ items: [{ productPublicId, name, sku?, mrp, sellingPrice }] }. All money in minor units.

Side Effects

None.

Error Cases

HTTPCodeCondition
400VALIDATION_FAILEDsearch shorter than 2 characters

8.3 POST /api/admin/pos/sales

Purpose

Open a new draft. customerId is required — a sale cannot exist without a customer, so an unknown walk-in is created via POST /sales/{id}/customer first. Always opens as a pickup; delivery is chosen later with PATCH /fulfilment.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_CREATE · Rate limit: ADMIN_POS_WRITE

Request

{ "paymentMethod": "pos_cash", "customerId": "018f1e2a-0000-7000-8000-000000000001" }

Response

201 — the PosSaleDto with empty items, status: "draft", fulfilment: "pickup".

Side Effects

pos_sale row + created audit event, same transaction.

Error Cases

HTTPCodeCondition
400validationBad method/customer
404POS_SALE_NO_CUSTOMERThe customer does not exist or is deleted

Edge Cases

  • paymentMethod is changeable until completion — the field is on the sale, not fixed at open.
  • Sending fulfilment is rejected outright (forbidNonWhitelisted).

8.4 GET /api/admin/pos/sales

Purpose

The reconciliation list (§51). Filters: status, fulfilment, payment method, customer, administrator, and a from/to date range on created_at.

from and to are read in Nepal time, and a date-only value means the whole local day.

@IsDateString() accepts a bare 2026-08-31, and new Date("2026-08-31") is midnight UTC — 05:45 in Kathmandu. As an inclusive upper bound that dropped everything sold after 05:45 on the last day asked for: a month-end reconciliation missed roughly three quarters of the 31st and read as a quiet day rather than a broken query. Nothing errored, and the total was plausible.

parseReportBoundary now expands from to the first instant of that day in Asia/Kathmandu and to to its last millisecond, so the two are contiguous — one day's end and the next day's start are 1ms apart, with no gap and no overlap.

A full timestamp is passed through untouched. A caller who sent an instant meant it, and widening that to a whole day is the mirror-image defect.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_READ · Rate limit: ADMIN_READ

Request

PartRequiredDetails
QueryNopage, size (≤100), status, fulfilment, paymentMethod, customerId, createdByAdminId, from, to

Response

200{ items: [PosSaleListItemDto], metadata: { count, page, size } }, newest first.

Side Effects

None. Page and total come from one transaction so they cannot disagree.

8.5 GET /api/admin/pos/sales/{publicId}

Purpose

One sale with its lines, for the detail view and the receipt.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_READ · Rate limit: ADMIN_READ

Response

200 — the full PosSaleDto. orderNumber/orderPublicId are NULL until completion.

Error Cases

HTTPCodeCondition
404POS_SALE_NOT_FOUNDThe sale id does not resolve

8.6 GET /api/admin/pos/sales/{publicId}/timeline

Purpose

The §42 audit trail, oldest first — who did what to this sale, in the order it happened.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_READ · Rate limit: ADMIN_READ

Response

200{ events: [{ publicId, eventType, fromStatus?, toStatus?, actorName?, reason?, createdAt }] }. actorName is NULL for sweep cancellations and for offboarded administrators.

Error Cases

HTTPCodeCondition
404POS_SALE_NOT_FOUNDThe sale id does not resolve

8.7 PATCH /api/admin/pos/sales/{publicId}/customer

Purpose

Point the draft at a customer who already has an account (e.g. after correcting a mistaken walk-in creation).

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_UPDATE · Rate limit: ADMIN_POS_WRITE

Request

{ "customerId": "018f1e2a-0000-7000-8000-000000000001" }

Response

200 — the refreshed PosSaleDto.

Side Effects

Changing the customer resets the sale to a pickup: the saved address belonged to one person, so fulfilment, deliveryAddressId and shippingAmount are cleared together (CLEAR_DELIVERY_ON_CUSTOMER_CHANGE). The operator must re-choose delivery for the new customer — re-render the fulfilment step from the response, never assume it survived. customerCreated is reset to false.

This applies to POST /customer (create a walk-in) exactly as it does to PATCH /customer. Until 2026-08-17 it did not: creating a walk-in customer cleared only the address, leaving fulfilment = 'delivery' beside a NULL delivery_address_id, which chk_pos_sale_fulfilment_address refuses. The ordinary sequence — choose delivery, type the address, then discover the customer has no account — produced a 23514 at the till, and because the account, its account row and the outbox invite all rolled back with it, retrying hit the same error indefinitely. The reset had been applied to one of the two call sites.

Error Cases

HTTPCodeCondition
404POS_SALE_NOT_FOUND / POS_SALE_NO_CUSTOMERMissing sale / missing customer
409POS_SALE_NOT_DRAFTCancelled
409POS_SALE_ALREADY_COMPLETEDMoney already taken

8.8 POST /api/admin/pos/sales/{publicId}/customer

Purpose

Create a walk-in account and attach it to the sale — account, invitation and sale update are ONE transaction.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_CREATE · Rate limit: ADMIN_POS_WRITE

Request

{ "name": "Sita Sharma", "email": "sita.sharma@example.com", "phone": "+9779812345678" }

Response

200{ sale, customerId, invitationQueued: true }.

Side Effects

  • customers + account rows (email verified FALSE — the operator typed it, nobody proved it; setting a password through the link is what proves it).
  • Outbox row pos.walk_in_created (invite queued at creation, not completion — the account exists either way).
  • Audit event customer_created (with the email; never the token).

The account is created with NO password. The customer receives an emailed set-password link that never expires but is single-use. There is nothing for a client to display, copy or resend. Never suggest a password is emailed.

Error Cases

HTTPCodeCondition
409POS_CUSTOMER_EMAIL_TAKENThe email already has an account (live or closed) — attach it instead
404POS_SALE_NOT_FOUNDMissing sale
409POS_SALE_NOT_DRAFT / POS_SALE_ALREADY_COMPLETEDNot a draft

8.9 PUT /api/admin/pos/sales/{publicId}/items/{productPublicId}

Purpose

Set the quantity of one product on the draft. The body is the FINAL quantity, not a delta — scanning the same barcode twice sends quantity: 2, not two calls of 1. Idempotent: sending the same quantity twice changes nothing.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_UPDATE · Rate limit: ADMIN_POS_WRITE

Request

{ "quantity": 2 }

Response

200 — the refreshed PosSaleDto with recomputed totals. The line freezes the product's current sellingPrice (and mrp) — a counter line is a price snapshot, unlike a cart line.

Side Effects

pos_sale_item upsert (ON CONFLICT (pos_sale_id, product_id) DO UPDATE), totals recomputed from the persisted lines, audit event item_added/item_quantity_changed.

Error Cases

HTTPCodeCondition
400POS_QUANTITY_OUT_OF_RANGENot a whole number in 1–99
404POS_PRODUCT_NOT_SELLABLEProduct missing, unpublished or deleted
409POS_PRODUCT_NOT_SELLABLEProduct priced above its MRP
404POS_SALE_NOT_FOUNDMissing sale
409POS_SALE_NOT_DRAFT / POS_SALE_ALREADY_COMPLETEDNot a draft

8.10 DELETE /api/admin/pos/sales/{publicId}/items/{productPublicId}

Purpose

Take an item back off the counter before payment. The line is hard-deleted — the record of what the customer actually bought is checkout_session_item and then order_item.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_UPDATE · Rate limit: ADMIN_POS_WRITE

Response

200 — the refreshed PosSaleDto with recomputed totals.

Error Cases

HTTPCodeCondition
404POS_LINE_NOT_FOUNDThe product is not on this sale
404POS_SALE_NOT_FOUNDMissing sale
409POS_SALE_NOT_DRAFT / POS_SALE_ALREADY_COMPLETEDNot a draft

8.11 PATCH /api/admin/pos/sales/{publicId}/fulfilment

Purpose

Choose how the goods leave the shop. The only endpoint that can take an address in the same request — either a saved one (addressPublicId) or a new one (address), which is saved to the customer's own address book.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_UPDATE · Rate limit: ADMIN_POS_WRITE

Request

Pickup:

{ "fulfilment": "pickup" }

Delivery with a new address:

{
  "fulfilment": "delivery",
  "shippingAmount": 15000,
  "address": {
    "districtId": 27, "municipalityName": "Lalitpur Metropolitan City", "ward": 5,
    "recipientName": "Sita Sharma", "recipientPhone": "+9779812345678",
    "street": "Jhamsikhel, Ward 5", "landmark": "Opposite the community school",
    "postalCode": "44700"
  }
}

Delivery with a saved address:

{ "fulfilment": "delivery", "addressPublicId": "018f1e2a-0000-7000-8000-000000000009" }

Response

200 — the refreshed PosSaleDto.

Side Effects

  • Pickup: clears the address and forces shippingAmount to 0.
  • Delivery: resolves the saved address (scoped to THIS customer in the query) or inserts a new customer_addresses row with is_default: false — the POS transaction deliberately does not take the default-address advisory lock.
  • grandTotal is recomputed in the same statement that writes shippingAmount, from the row's own subtotal and discountAmount.

That last point is not an optimisation. chk_pos_sale_grand_total_matches asserts grand_total = subtotal − discount_amount + shipping_amount, and the constraint is not deferrable — PostgreSQL evaluates it at the end of the statement, not at commit.

Writing shipping_amount alone and repairing the total on the next line therefore cannot work: the first statement is already rejected and the whole transaction aborts, taking the newly-inserted address row with it. That was the behaviour until 2026-08-17, which meant no non-zero delivery charge could ever be saved — every attempt was a bare 23514 at the till.

It survived type-check, lint, build, five rule gates and the full integration suite because every delivery fixture used shippingAmount: 0, where the identity holds trivially. When adding a fixture for a money path, give it a non-zero value.

Error Cases

HTTPCodeCondition
409POS_DELIVERY_ADDRESS_REQUIREDDelivery with no address — or both addressPublicId AND address (mutually exclusive)
404POS_ADDRESS_NOT_FOUNDThe address is not this customer's
404POS_SALE_NOT_FOUNDMissing sale
409POS_SALE_NOT_DRAFT / POS_SALE_ALREADY_COMPLETEDNot a draft

8.12 POST /api/admin/pos/sales/{publicId}/complete

Purpose

Take the money and produce the order — ONE transaction: stock moves, the payment is recorded, the order is created, and for a pickup the goods are handed over. There is no state in which a customer has paid and no order exists. Returns the order number for the receipt.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_CREATE · Rate limit: ADMIN_POS_COMPLETE (30/min — each call takes money)

Request

{ "paymentReference": "TXN-88213" }

paymentReference is optional and permitted only for pos_card and pos_qr. Sending one with pos_cash is rejected. It is recorded, never validated — the platform does not check it against any provider, and a client must not present it as verified.

Response

200{ sale (status picked_uporordered), orderNumber, orderPublicId }.

Side Effects

Cart (born converted), checkout session (born completed, channel: "pos"), frozen checkout_session_items, settled payment_attempt (born succeeded, gateway_transaction_id = POS-{saleNumber}), reservation finalize, order (createFromCheckout with executor: tx), pickup hand-over to delivered, outbox pos.sale_completed, events payment_recorded + completed, and post-commit invalidation of the inventory cache domain.

Error Cases

HTTPCodeCondition
409POS_SALE_EMPTYNo lines
409POS_SALE_NO_CUSTOMERNo customer attached
409POS_DELIVERY_ADDRESS_REQUIREDDelivery with no address
409POS_PRODUCT_NOT_SELLABLEA product is no longer published
409POS_PRODUCT_PRICE_CHANGEDA price moved mid-sale — tell the operator to speak to the customer; remove and re-add to charge the new price
409POS_INSUFFICIENT_STOCKSomeone else took the last one — show remaining stock, let the operator reduce the quantity
409POS_PAYMENT_REFERENCE_NOT_ALLOWEDReference sent with cash
409POS_SALE_ALREADY_COMPLETEDThe money was already taken — do not retry, show the existing order number
409POS_ADDRESS_NOT_FOUNDThe configured store district does not exist (pickup shipping resolve)

POS_PRODUCT_PRICE_CHANGED and POS_INSUFFICIENT_STOCK mean nothing was charged and nothing was reserved — the whole completion rolled back, so the sale is still a live draft.

Edge Cases

  • Replayed completion: the four partial unique indexes (uq_pos_sale_{cart,checkout_session,payment_attempt,order}_id) plus the guarded WHERE status = 'draft' finalise make a retry a 409, never a second charge.
  • A delivery sale's order stays at confirmed for the normal lifecycle; a pickup's order is driven straight to delivered (return window starts, purchase reviewable).

8.13 POST /api/admin/pos/sales/{publicId}/cancel

Purpose

Abandon a draft before any money has moved. Only reachable from draft — once completed, the correction is an order cancellation and refund, owned by the Order module.

Auth and Permissions

  • Auth: JwtAuthGuard + RoleGuard · Permission: Pos_DELETE · Rate limit: ADMIN_POS_WRITE

Request

{ "reason": "Customer changed their mind" }

Response

200 — the PosSaleDto with status: "cancelled".

Error Cases

HTTPCodeCondition
400validationBlank or missing reason
404POS_SALE_NOT_FOUNDMissing sale
409POS_SALE_NOT_DRAFT / POS_SALE_ALREADY_COMPLETEDNot a draft

9. Flow Diagrams

9.1 Route Ownership

9.2 Request Sequence (complete)

9.3 Error Branch (completion)

EndpointPagination TypeDefault SizeMax SizeSort FieldsFiltersResult Cap
GET /api/admin/pos/salesoffset page/size20100createdAt DESC, id DESC (fixed)status, fulfilment, paymentMethod, customerId, createdByAdminId, from, to10k/100k rows (offset cap, ~3 years at 100 sales/day)
GET /api/admin/pos/lookup/customersoffset page/size20100name ASC, id ASC (fixed)search (≥2 chars, ILIKE)
GET /api/admin/pos/lookup/productsoffset page/size20100name ASC, id ASC (fixed)search (≥2 chars, ILIKE on name/SKU)

11. Caching, Jobs, and External Integrations

IntegrationUsed?DetailsSource
Redis cacheYes (invalidation only)inventory domain cleared after a completed sale (triggerForWrite, post-commit, fire-and-forget)pos.constants.ts
BullMQYesQueueName.POSpos.send_receipt, pos.send_walk_in_invite (outbox, same tx), pos.sweep_abandoned_drafts (hourly cron, direct). One @Processor (concurrency 3) dispatches via an exhaustive record; HttpException = deterministic failure, never retriedpos-queue.processor.ts
External APINoPayment references are recorded, never validated

13. Mandatory Deep API Documentation Pack

13.1 Route-by-Route Completeness Matrix

RouteController MethodDTOsService MethodGuardsPermissionsCacheJobsDB TouchesErrorsDocumented?
GET /api/admin/pos/lookup/customerssearchCustomersPosLookupQueryDtoPosLookupService.searchCustomersJWT+Role+ThrottlePos_READcustomers400Yes
GET /api/admin/pos/lookup/productssearchProductsPosLookupQueryDtoPosLookupService.searchProductsJWT+Role+ThrottlePos_READproducts400Yes
POST /api/admin/pos/salesopenOpenPosSaleDtoPosSaleDraftService.openJWT+Role+ThrottlePos_CREATEpos_sale, event400/404Yes
GET /api/admin/pos/salesfindAllListPosSalesQueryDtoPosSaleQueryService.findAllJWT+Role+ThrottlePos_READpos_sale + joinsYes
GET /api/admin/pos/sales/{publicId}findOnePosSaleQueryService.findOneJWT+Role+ThrottlePos_READpos_sale, items404Yes
GET /api/admin/pos/sales/{publicId}/timelinefindTimelinePosSaleQueryService.findTimelineJWT+Role+ThrottlePos_READevents404Yes
PATCH /api/admin/pos/sales/{publicId}/customerattachCustomerAttachPosCustomerDtoPosSaleDraftService.attachCustomerJWT+Role+ThrottlePos_UPDATEpos_sale, event404/409Yes
POST /api/admin/pos/sales/{publicId}/customercreateWalkInCreatePosWalkInDtoPosSaleDraftService.createWalkInCustomerJWT+Role+ThrottlePos_CREATEoutbox invitecustomers, account, pos_sale, outbox, event404/409Yes
PUT /api/admin/pos/sales/{publicId}/items/{productPublicId}upsertLineUpsertPosLineDtoPosSaleLineService.upsertLineJWT+Role+ThrottlePos_UPDATEpos_sale_item, pos_sale, event400/404/409Yes
DELETE /api/admin/pos/sales/{publicId}/items/{productPublicId}removeLinePosSaleLineService.removeLineJWT+Role+ThrottlePos_UPDATEpos_sale_item, pos_sale, event404/409Yes
PATCH /api/admin/pos/sales/{publicId}/fulfilmentsetFulfilmentSetPosFulfilmentDtoPosSaleDraftService.setFulfilmentJWT+Role+ThrottlePos_UPDATEpos_sale, address, event404/409Yes
POST /api/admin/pos/sales/{publicId}/completecompleteCompletePosSaleDtoPosSaleCompletionService.completeJWT+Role+ThrottlePos_CREATEinventory (after commit)outbox receipt7 tables + events + outbox409 setYes
POST /api/admin/pos/sales/{publicId}/cancelcancelCancelPosSaleDtoPosSaleDraftService.cancelJWT+Role+ThrottlePos_DELETEpos_sale, event400/404/409Yes

13.2 Request/Response Exhaustiveness

Covered in §8: minimal and full payloads for every body endpoint (§6, §8.3/8.8/8.11/8.12), the final-quantity semantics of PUT (§8.9), the mutually-exclusive address fields (§8.11), the pickup-reset on customer change (§8.7), the never-expiring single-use invite (§8.8), the full PosSaleDto shape (§6.10), every nullable field, and per-endpoint error tables with the exact codes and the "nothing was charged" meaning of the two completion conflicts (§8.12).

13.3 API Diagram Pack

Route ownership (§9.1), completion sequence (§9.2), completion error tree (§9.3), plus the activity/till flow and queue topology in the feature and backend docs.

13.4 Consumer Integration Notes

ConsumerRequired KnowledgeFailure HandlingContract Stability
Admin panel (till)13 routes, Pos_* permissions, final-quantity PUT, pickup-first drafts, recorded:false-style semantics for references409s → re-fetch the sale; POS_SALE_ALREADY_COMPLETED → show the order number, never retryStable
Admin panel (reports)GET /sales filters, integer minor units, customerCreated, createdByAdminIdOffset cap at 10k rowsStable
Admin panel (walk-in)No password ever; invitationQueued: true is the whole answer; never expect a token409 POS_CUSTOMER_EMAIL_TAKEN → switch to attachStable
Storefront frontendNothing changes. order.channel is "online" for every customer-placed order; the new payment methods never appear in the customer-selectable listStable
Admin panel (order rendering)order.payment_method may now be pos_cash/pos_card/pos_qr — add labels or it shows a raw enumDegrades to an ugly label, never an errorStable
QAConstraint invariants (status/key pairs, pickup no-shipping), one-transaction completion, replay safetyReproduce via the exact codesStable

13.5 API Tradeoffs and Rationale

DecisionChosen BehaviorAlternatives ConsideredWhy This TradeoffRiskMitigation
No fulfilment on openDrafts always open pickupAccepting deliveryopen() cannot supply an address; the old field was a guaranteed 23514One extra tapRe-render fulfilment from responses
PUT replaces quantityIdempotent final quantityPOST incrementA till types a numberDouble-scan confusionDocumented; ON CONFLICT
customerId required at openNo customerless draftAnonymous salesEvery sale belongs to an accountExtra step for new walk-insWalk-in endpoint first
Reference never validatedRecorded as enteredProvider check§17 leaves verification with the operatorFalse "verified" impressionDocumented, refused beside cash
Completion is one txAll-or-nothingPipeline with compensationThe paid-without-order state is not cleanableLong lock scopeAscending lock order
Customer change resets to pickupAddress/fulfilment/shipping clearedPartial clearThe old address is not this person'sRe-picking deliveryExplicit in docs
Walk-in invite never expiresFar-future sentinel, single use24h TTLOwner policy; support dead-endLong-lived if stolenSingle use, no OTP
New address not the defaultis_default: falseTaking the default lockPOS tx must not race the address advisory lockMinor UXCustomer promotes later

13.6 API Change Impact

ChangeAffected ConsumersBackend ImpactData ImpactMigration Needed?Compatibility Plan
order.channel / checkout_session.channel addedStorefront (ignores), reportingColumn + enumNone — defaults onlineNoAdditive; existing rows unaffected
payment_method gains 3 valuesAdmin order rendererEnum + gateway labelsNoneNoLabel the new values; storefront list unchanged
Any future lookup changeTillPosLookupServiceNoneNoContract frozen in consumer-handoff
Future barcode columnTillProduct schemaMigrationYes (backend change, not made)Scanner emits SKU today

14. Zero-Omission API Checklist

  • Every controller route is documented (§4, §8, §13.1).
  • Every parent route prefix and runtime URL is documented (§2, §4).
  • Every DTO field, nested field, enum, default, transform, and validator is documented (§6, §7).
  • Every response field, nullable field, generated field, and omitted raw entity field is documented (§6.10, §8).
  • Every auth, guard, permission, role, public decorator, and guest identity branch is documented (§5) — there is no public/guest surface by design.
  • Every success, validation, auth, permission, not-found, conflict, rate-limit, and server-error branch is documented (§8).
  • Every database read/write, cache invalidation, queue job, notification, and audit event is documented (§8 side effects, §11).
  • Every route has examples for minimal request, full request, success response, and representative failures (§8).
  • Every endpoint family has route, sequence, and error diagrams (§9).
  • Every tradeoff and compatibility risk is documented (§13.5, §13.6).
  • The API doc links to backend and features/flows (See Also).

15. Integration Checklist

  • Every route from controllers is documented.
  • Every DTO field is documented.
  • Every enum value is documented.
  • Every response envelope is documented.
  • Every error code is documented.
  • Every auth guard and permission is documented.
  • Every cache key, queue job, and external call is documented.
  • Every diagram matches the current code.
  • The API doc links to backend and features/flows.

See Also

  • Backend doc: /docs/developer/pos/backend
  • Features and flows doc: /docs/developer/pos/feature
  • TDD: not yet published

On this page

POS - API Reference1. Documentation Evidence2. Module Summary3. Concepts and Terminology4. API Surface Map5. Auth, Identity, and Permissions6. DTO and Model Reference6.1 OpenPosSaleDtoPOST /sales6.2 AttachPosCustomerDtoPATCH /sales/{publicId}/customer6.3 CreatePosWalkInDtoPOST /sales/{publicId}/customer6.4 UpsertPosLineDtoPUT /sales/{publicId}/items/{productPublicId}6.5 SetPosFulfilmentDtoPATCH /sales/{publicId}/fulfilment6.6 CompletePosSaleDtoPOST /sales/{publicId}/complete6.7 CancelPosSaleDtoPOST /sales/{publicId}/cancel6.8 ListPosSalesQueryDtoGET /sales6.9 PosLookupQueryDto — both lookup routes6.10 Response DTOs7. Enum Reference8. Endpoint Reference8.1 GET /api/admin/pos/lookup/customers?search=PurposeAuth and PermissionsRequestResponseSide EffectsError Cases8.2 GET /api/admin/pos/lookup/products?search=PurposeAuth and PermissionsRequestResponseSide EffectsError Cases8.3 POST /api/admin/pos/salesPurposeAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.4 GET /api/admin/pos/salesPurposeAuth and PermissionsRequestResponseSide Effects8.5 GET /api/admin/pos/sales/{publicId}PurposeAuth and PermissionsResponseError Cases8.6 GET /api/admin/pos/sales/{publicId}/timelinePurposeAuth and PermissionsResponseError Cases8.7 PATCH /api/admin/pos/sales/{publicId}/customerPurposeAuth and PermissionsRequestResponseSide EffectsError Cases8.8 POST /api/admin/pos/sales/{publicId}/customerPurposeAuth and PermissionsRequestResponseSide EffectsError Cases8.9 PUT /api/admin/pos/sales/{publicId}/items/{productPublicId}PurposeAuth and PermissionsRequestResponseSide EffectsError Cases8.10 DELETE /api/admin/pos/sales/{publicId}/items/{productPublicId}PurposeAuth and PermissionsResponseError Cases8.11 PATCH /api/admin/pos/sales/{publicId}/fulfilmentPurposeAuth and PermissionsRequestResponseSide EffectsError Cases8.12 POST /api/admin/pos/sales/{publicId}/completePurposeAuth and PermissionsRequestResponseSide EffectsError CasesEdge Cases8.13 POST /api/admin/pos/sales/{publicId}/cancelPurposeAuth and PermissionsRequestResponseError Cases9. Flow Diagrams9.1 Route Ownership9.2 Request Sequence (complete)9.3 Error Branch (completion)10. Pagination, Sorting, Filtering, and Search11. Caching, Jobs, and External Integrations13. Mandatory Deep API Documentation Pack13.1 Route-by-Route Completeness Matrix13.2 Request/Response Exhaustiveness13.3 API Diagram Pack13.4 Consumer Integration Notes13.5 API Tradeoffs and Rationale13.6 API Change Impact14. Zero-Omission API Checklist15. Integration ChecklistSee Also