Point of Sale

POS configuration, orders, covers, and slots inside the Sales plugin, plus the sync API the standalone POS client uses.

Two services are involved: sales_api (this module, port 3305) stores POS records and orders, and posclient_api (port 3312) serves the offline-capable storefront app. For the storefront itself, see POS Client.

Module layout

Backend code lives under backend/plugins/sales_api/src/modules/pos/:

AreaPathResponsibility
Modelsdb/definitions/{pos,orders,covers}.ts, db/modelsPos, PosOrders, PosCovers, PosSlots, ProductGroups
GraphQLgraphql/schemas, graphql/resolversPOS config, order, cover, product-group operations
HTTP routesroutes.ts (mounted from src/routes.ts)/pos-init, /pos-sync-config, /get-pos-token
tRPCtrpc/pos.tspos.* and orders.* routers
Metameta/automations, meta/segments, meta/exportPOS-order event trigger, sales:pos.orders segment type, POS-items export

POS settings screens live in frontend/plugins/sales_ui/src/modules/pos/ under settings/sales/*.

Data model

  • Pos: one document per terminal/store: name, token, adminIds/cashierIds, isOnline/onServer, branchId/departmentId, allowBranchIds, paymentIds, paymentTypes, productDetails, catProdMappings, initialCategoryIds, deliveryConfig, kioskMachine/kitchenScreen/waitingScreen, permissionConfig (e.g. cashiers.seeReport), isCheckRemainder/saveRemainder, beginNumber/skipNumber.
  • PosOrders: synced orders: number, status, paidDate, customerId/customerType, posToken, items, paidAmounts, cashAmount/mobileAmount, totalAmount, deliveryInfo (may carry dealId), subscriptionInfo, registerNumber, convertDealId.
  • PosCovers: shift/cash covers with a note.
  • PosSlots and ProductGroups: table slots and the per-POS category groupings (with categoryIds, excludedCategoryIds, excludedProductIds) that shape the storefront catalog.

HTTP routes

Mounted on the plugin's Express app (modules/pos/routes.ts), reached through the gateway as /pl:sales/<path>:

RouteHandlerPurpose
GET /pos-initposInitFull bootstrap for a POS token (header pos-token): config + users + product groups + slots
POST /pos-sync-configposSyncConfigPartial resync by type: config, products, slots, productsConfigs
GET /get-pos-tokengetPosTokenLists { name, token } for every POS; requires ?GET_CP_TOKEN= matching the GET_CP_TOKEN env var

posInit/posSyncConfig assemble data across services: admins and cashiers come from Core users.find, and product categories/products from Core productCategories.find/products.find, all over tRPC. They also pull tax-receipt settings and pricing discounts from Enterprise Edition plugins.

Protect GET_CP_TOKEN

GET /get-pos-token returns POS tokens for the whole tenant. Keep GET_CP_TOKEN secret; it is meant for the client-portal flow, not general integrations.

Queries and mutations

Schemas live under modules/pos/graphql/schemas.

OperationKindPurpose
posList / posDetail(_id) / posEnvqueryPOS configs; posEnv exposes ALLOW_OFFLINE_POS
posAdd / posEdit / posRemovemutationPOS lifecycle
productGroups / productGroupsAdd / productGroupsBulkInsertquery/mutationCatalog grouping per POS
posSlots / posSlotBulkUpdatequery/mutationTable slots
posProducts(…)queryCatalog fetch against a POS scope
posOrders / posOrdersList / posOrderDetail / posOrderLinkqueryOrder lists and detail
posOrdersSummary / posOrdersGroupSummary / posOrdersTotalCount / posOrderRecords / posOrderRecordsCountqueryAggregates and item records
posOrderCustomers(+TotalCount) / posOrderBySubscriptions(+TotalCount) / checkSubscriptionqueryCustomer and subscription views
posOrderChangePayments(_id, …)mutationAdjust payments on an order
posCovers / posCoversCount / posCoverDetail / posCoversEdit / posCoversRemovequery/mutationCovers
ecommerceGetBranches(posToken)queryBranch list for a POS token

Sync with posclient_api

The storefront backend pulls config and pushes orders:

  • Config pull: posConfigsFetch(token) on posclient calls GET /pl:sales/pos-init and stores the result in its Configs collection; syncConfig(type) re-pulls config/products/slots/productsConfigs through POST /pl:sales/pos-sync-config.
  • Order push: syncOrders on posclient batches up to 100 unsynced paid orders (with items and receipt responses) and calls pos.createOrUpdateOrdersMany over tRPC; pos.createOrUpdateOrders handles single-order statusToDone and payment sync.
  • Hourly remainder sync: posclient_api's BullMQ scheduler posclient-sync-remainder (0 * * * * UTC) runs syncRemainders/syncDiscounts per tenant for every config with saveRemainder.
  • Payment callbacks: meta.payments.callback in sales_api forwards paid sales:pos.orders transactions to posclient.paymentCallbackClient over tRPC.

tRPC

trpc/pos.ts exposes pos and orders sub-routers: pos.findOne/find/create/confirmCover/ecommerceGetBranches/ordersDeliveryInfo/createOrUpdateOrders/createOrUpdateOrdersMany, and orders.findOne/find/updateOne.

Automations and segments

modules/pos/meta/automations registers a custom trigger "POS order event" (sales:pos.orders) with event types created, paid, returned, statusChanged, paymentChanged, deliveryCompleted, and an action that creates POS orders. The sales:pos.orders segment content type is declared under meta/segments/ with field filters, member listing, and a customer.orders relation; it needs an index on pos_orders.customerId for relation measures.

Permissions

The pos permission module registers posRead, posOrderRead, posCoversRead (all always), plus posAdd/posEdit/posRemove, posOrderChangePayments, posCoversEdit/posCoversRemove, posSlotBulkUpdate, productGroupsBulkInsert, and posItemsExportManage (the sales:pos.posItems export type).

Troubleshooting

  • pos-init returns "Not found POS by token": the pos-token header is missing or doesn't match a Pos document's token.
  • pos-sync-config returns "wrong type": type must be one of config, products, slots, productsConfigs.
  • Remainders never sync: saveRemainder must be set on the POS config, and the posclient worker must be running.
  • New POS is forced online: without ALLOW_OFFLINE_POS, pos.create sets onServer, which triggers syncPosToClient on save.
Was this helpful?