# Point of Sale

POS configuration, orders, sync routes, and the posclient API


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](https://erxes.io/docs/app-pos-client).

## Module layout

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

| Area | Path | Responsibility |
| --- | --- | --- |
| Models | `db/definitions/{pos,orders,covers}.ts`, `db/models` | `Pos`, `PosOrders`, `PosCovers`, `PosSlots`, `ProductGroups` |
| GraphQL | `graphql/schemas`, `graphql/resolvers` | POS config, order, cover, product-group operations |
| HTTP routes | `routes.ts` (mounted from `src/routes.ts`) | `/pos-init`, `/pos-sync-config`, `/get-pos-token` |
| tRPC | `trpc/pos.ts` | `pos.*` and `orders.*` routers |
| Meta | `meta/automations`, `meta/segments`, `meta/export` | POS-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`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/pos/routes.ts)), reached through the gateway as `/pl:sales/<path>`:

| Route | Handler | Purpose |
| --- | --- | --- |
| `GET /pos-init` | `posInit` | Full bootstrap for a POS token (header `pos-token`): config + users + product groups + slots |
| `POST /pos-sync-config` | `posSyncConfig` | Partial resync by `type`: `config`, `products`, `slots`, `productsConfigs` |
| `GET /get-pos-token` | `getPosToken` | Lists `{ 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.

> [!WARNING]
> **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`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/pos/graphql/schemas).

| Operation | Kind | Purpose |
| --- | --- | --- |
| `posList` / `posDetail(_id)` / `posEnv` | query | POS configs; `posEnv` exposes `ALLOW_OFFLINE_POS` |
| `posAdd` / `posEdit` / `posRemove` | mutation | POS lifecycle |
| `productGroups` / `productGroupsAdd` / `productGroupsBulkInsert` | query/mutation | Catalog grouping per POS |
| `posSlots` / `posSlotBulkUpdate` | query/mutation | Table slots |
| `posProducts(…)` | query | Catalog fetch against a POS scope |
| `posOrders` / `posOrdersList` / `posOrderDetail` / `posOrderLink` | query | Order lists and detail |
| `posOrdersSummary` / `posOrdersGroupSummary` / `posOrdersTotalCount` / `posOrderRecords` / `posOrderRecordsCount` | query | Aggregates and item records |
| `posOrderCustomers(+TotalCount)` / `posOrderBySubscriptions(+TotalCount)` / `checkSubscription` | query | Customer and subscription views |
| `posOrderChangePayments(_id, …)` | mutation | Adjust payments on an order |
| `posCovers` / `posCoversCount` / `posCoverDetail` / `posCoversEdit` / `posCoversRemove` | query/mutation | Covers |
| `ecommerceGetBranches(posToken)` | query | Branch 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`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/pos/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.

