This commit is contained in:
+39
-36
@@ -28,47 +28,48 @@ Swagger UI: `http://localhost:3001/api/docs`
|
||||
### Venues
|
||||
`GET /venues` - List venues
|
||||
`GET /venues/:venueId` - Get venue details
|
||||
`GET /venues/:venueId/devices` - List devices in venue
|
||||
`GET /venues/:venueId/devices` - The venue's paired terminals `{ devices: [...] }`
|
||||
|
||||
### Devices
|
||||
`POST /devices/register` - Register a new device
|
||||
`POST /devices/register` - Pair a terminal; every pairing adds one
|
||||
`POST /devices/:deviceId/sync` - Sync device state (device auth)
|
||||
`POST /devices/:deviceId/commands/:commandId/ack` - Acknowledge command (device auth)
|
||||
`GET /devices/generation-code` - Generate registration code (admin)
|
||||
|
||||
Sync semantics:
|
||||
- `openTabs` must be the device's **complete** list of open tabs. Tabs it no longer reports
|
||||
(e.g. deleted on the terminal) are removed from the dashboard. Settling a tab on the terminal
|
||||
keeps the tab open under the same name with a zero balance, so the name stays listed.
|
||||
- Every sync returns **all** pending commands for the device; `cursor` is still returned but no longer
|
||||
filters. Devices dedupe by command id and re-send the ack for commands they already processed.
|
||||
- The first ack for a command wins; later acks are accepted as no-ops.
|
||||
- `plannedTabNames` only contains planned customers the device has not opened a tab for yet. Once a sync
|
||||
reports an open tab with that name (ignoring case and extra spaces), the customer is marked delivered
|
||||
and no longer sent. The customer (and their email, used for reminders) is kept.
|
||||
- `events` (optional) reports what happened on tabs since the last sync, for sales statistics. Each event
|
||||
has a terminal-generated UUID `id`, a `type`, `tabId`, `customerName` and `occurredAt` (ISO-8601, UTC):
|
||||
- `line_added` / `line_removed`: `productName`, `unitPriceInCents`, `quantity` (always positive)
|
||||
- `settled`: `amountInCents`, `paymentMethod` (`cash`, `payconiq`, `payment_pending` or `other`),
|
||||
optional `commandId` (the remote close that caused it) and `items` (the settled lines)
|
||||
Sync semantics. The server holds the venue's open tabs and stock; a venue can have several terminals. Each terminal
|
||||
works offline, queues what it does as **events**, and on every sync replaces its tabs and stock with the server's and
|
||||
replays its unsent events on top. The full rules are in the app repository: `docs/server-tabs-source-of-truth.md`
|
||||
and `docs/server-stock-source-of-truth.md`.
|
||||
- Every write to a venue's tabs, stock and settlements (syncs, dashboard settles, stock adjustments) runs in one
|
||||
transaction that locks the venue row, so simultaneous terminals apply their changes one after the other.
|
||||
- `events` (at most 1000): terminal-generated UUID `id`, `type`, `occurredAt` (ISO-8601, UTC; capped at the server
|
||||
time when more than 5 minutes ahead) and per type:
|
||||
- `tab_opened` (`tabId`, `customerName`, `openedAt`), `tab_renamed` (new `customerName`), `tab_deleted`
|
||||
- `line_added` / `line_removed`: `lineId`, `productId`, `productName`, `unitPriceInCents`, `quantity` (≥ 1)
|
||||
- `settled`: `amountInCents`, `paymentMethod` (`cash`, `payconiq`, `payment_pending` or `other`), optional
|
||||
`commandId` and `items` (`[{ productId?, productName, quantity, unitPriceInCents }]`)
|
||||
- `stock_set`: `productId`, `quantity` (a count ≥ 0, or `null` to stop tracking); no `tabId`
|
||||
|
||||
The response lists `acknowledgedEventIds` (stored, including events already stored by an earlier sync)
|
||||
and `rejectedEvents` (`[{ id, reason }]`, invalid and never stored). The terminal removes both from its
|
||||
outbox. An invalid event never fails the sync. At most 1000 events per sync.
|
||||
- `products` (optional) is the terminal's **complete** product catalog:
|
||||
`[{ id, name, category?, priceInCents, stock }]`, where `stock` is an integer (may be negative) or `null` when
|
||||
the terminal doesn't track stock for that product. Products left out are removed from the dashboard. When the
|
||||
field is omitted (older app versions) the stored products are left alone. An invalid product is skipped and
|
||||
logged, never failing the sync. At most 2000 products.
|
||||
- `stockAdjustments` in the response lists **every** pending stock change requested from the dashboard
|
||||
for the venue, oldest first: `[{ id, productId, type, quantity, note, requestedBy, createdAt }]`.
|
||||
`type: "add"` changes the stock by `quantity` (negative takes stock out); `type: "set"` makes the stock
|
||||
`quantity` (a count, ≥ 0, and starts tracking an untracked product). They are re-sent on every sync until the
|
||||
terminal reports an outcome, so the terminal must apply each id only once.
|
||||
- `stockAdjustmentResults` (optional) reports outcomes: `[{ id, status: "applied" | "rejected", stockAfter?, reason? }]`
|
||||
(`stockAfter` is required for `applied`). The response lists `acknowledgedStockAdjustmentResultIds` and
|
||||
`rejectedStockAdjustmentResults` (`[{ id, reason }]`); the terminal drops both from its outbox. The terminal's
|
||||
outcome is recorded even if the adjustment was cancelled on the dashboard in the meantime.
|
||||
Each id is applied once (tab state and stock in the same transaction). `acknowledgedEventIds` lists the applied
|
||||
ones, including repeats; `rejectedEvents` (`[{ id, reason }]`) were invalid and never applied. The terminal drops
|
||||
both from its outbox. An invalid event never fails the sync.
|
||||
- The response's `tabs` is the venue's **complete** open tab state after this request's events:
|
||||
`[{ id, customerName, openedAt, version, lines: [{ id, productId, productName, unitPriceInCents, quantity,
|
||||
units: [{ id: "<event id>:<index>", addedAt, removedAt }] }] }]`.
|
||||
- `stock` is every product of the venue: `[{ productId, stock, countedAt }]` (`stock` null when not tracked, may be
|
||||
negative). Line events change it unless they happened before `countedAt`; the latest count wins.
|
||||
- `settlements` lists the venue's settlements after `settlementsCursor` (or of the last 30 days without one), oldest
|
||||
first, at most 500; the response's `settlementsCursor` is sent back next time.
|
||||
- `openTabs` is only used once per venue, on the first sync after the server started holding the tabs: the terminal's
|
||||
own tabs (and catalog stock) are taken over and the events of that request are stored without applying them again.
|
||||
- `products` (optional, at most 2000) upserts names, categories and prices. Its `stock` is ignored (counts arrive as
|
||||
`stock_set`), a new product starts untracked, and products are never removed because one terminal doesn't report
|
||||
them. An invalid product is skipped and logged.
|
||||
- Planned customers are opened as tabs by the server, once; `plannedTabNames` is always empty.
|
||||
- Dashboard stock adjustments are applied by the server right away; `stockAdjustments` is always empty.
|
||||
`stockAdjustmentResults` from older app versions are still recorded and acknowledged.
|
||||
- Every sync returns **all** pending commands for the device (only `close_tab` commands created before the server
|
||||
settled tabs itself); `cursor` is still returned but no longer filters. The first ack for a command wins.
|
||||
|
||||
### Tabs
|
||||
`GET /venues/:venueId/tabs` - List open tabs
|
||||
@@ -98,9 +99,11 @@ Names match case-insensitively and keep their existing spelling; `replace` also
|
||||
`PUT /venues/:venueId/stock/products/:productId` - Set `{ lowStockThreshold: number | null }` (admin, dashboard only)
|
||||
|
||||
### Commands
|
||||
`POST /venues/:venueId/commands` - Create a command
|
||||
`POST /venues/:venueId/commands` - Settle a tab: `{ tabId, type: "close_tab", expectedVersion, paymentMethod }`. The
|
||||
server settles everything on the tab at once (also with every terminal offline) and records the command as
|
||||
`completed`. `version_mismatch` when the tab changed since it was shown, `tab_empty` when there is nothing on it.
|
||||
`GET /venues/:venueId/commands` - List command history
|
||||
`POST /venues/:venueId/commands/devices/:deviceId/revoke` - Revoke device (admin)
|
||||
`POST /venues/:venueId/commands/devices/:deviceId/revoke` - Unpair a terminal (admin)
|
||||
|
||||
All `/venues/:venueId/...` routes return `403 venue_forbidden` unless the user belongs to that venue.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user