129 lines
7.2 KiB
Markdown
129 lines
7.2 KiB
Markdown
# API Documentation
|
|
|
|
Base URL: `http://localhost:3001/api/v1`
|
|
|
|
Swagger UI: `http://localhost:3001/api/docs`
|
|
|
|
## Authentication
|
|
|
|
### Dashboard Users
|
|
- Login to receive JWT access token (24h) + refresh token (30 days)
|
|
- Include access token in `Authorization: Bearer <token>` header
|
|
|
|
### Devices
|
|
- Register to receive device bearer token (90 days)
|
|
- Include device token in `Authorization: Bearer <token>` header
|
|
- Device ID must match URL parameter
|
|
|
|
## Endpoints
|
|
|
|
### Health
|
|
`GET /health` - Server health check (503 when the database is unreachable)
|
|
|
|
### Auth
|
|
`POST /auth/login` - Login with email/password
|
|
`POST /auth/refresh` - Refresh access token
|
|
`POST /auth/logout` - Logout and revoke refresh token
|
|
|
|
### Venues
|
|
`GET /venues` - List venues
|
|
`GET /venues/:venueId` - Get venue details
|
|
`GET /venues/:venueId/devices` - The venue's paired terminals `{ devices: [...] }`
|
|
|
|
### Devices
|
|
`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. 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`
|
|
|
|
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
|
|
|
|
### Planned Tabs (customer list)
|
|
`GET /venues/:venueId/planned-tabs` - List planned customers
|
|
`POST /venues/:venueId/planned-tabs` - Add a customer (admin)
|
|
`POST /venues/:venueId/planned-tabs/import` - Bulk import `{ entries: [{ customerName, email? }], mode: 'merge' | 'replace' }` (admin).
|
|
Names match case-insensitively and keep their existing spelling; `replace` also removes customers missing from the import.
|
|
`PUT /venues/:venueId/planned-tabs/:id` - Rename a customer / set email (admin)
|
|
`DELETE /venues/:venueId/planned-tabs/:id` - Remove a customer (admin)
|
|
|
|
### Reminders
|
|
`GET /venues/:venueId/reminders/candidates` - Customers with an unpaid balance, with their reminder count, last reminder
|
|
and whether they are eligible (have an email and weren't reminded in the last 7 days)
|
|
`POST /venues/:venueId/reminders/send` - Send payment reminders (admin). Body `{ "tabIds": [...] }` picks the
|
|
recipients, including customers still in their cooldown; without it every eligible customer is reminded
|
|
`GET /venues/:venueId/reminders?customerName=` - Reminder history, optionally for one customer
|
|
`POST /venues/:venueId/reminders/test/:plannedTabId` - Send a test email (admin)
|
|
|
|
### Stock
|
|
`GET /venues/:venueId/stock` - The terminal's products with stock, low stock level and pending adjustments
|
|
`GET /venues/:venueId/stock/adjustments?productId=&limit=&offset=` - Stock adjustment history, newest first
|
|
`POST /venues/:venueId/stock/adjustments` - Request stock changes (admins and viewers): `{ adjustments: [{ productId, type: 'add' | 'set', quantity, note? }] }`.
|
|
`add` takes a non-zero quantity (negative for breakage) and needs a tracked product; `set` takes 0 or more.
|
|
`POST /venues/:venueId/stock/adjustments/:id/cancel` - Cancel an adjustment the terminal hasn't received yet (admins and viewers)
|
|
`PUT /venues/:venueId/stock/products/:productId` - Set `{ lowStockThreshold: number | null }` (admin, dashboard only)
|
|
|
|
### Commands
|
|
`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` - Unpair a terminal (admin)
|
|
|
|
All `/venues/:venueId/...` routes return `403 venue_forbidden` unless the user belongs to that venue.
|
|
|
|
## Error Format
|
|
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "error_code",
|
|
"message": "Human-readable message"
|
|
}
|
|
}
|
|
```
|
|
|
|
## HTTP Status Codes
|
|
- 200 Success
|
|
- 201 Created
|
|
- 400 Bad Request
|
|
- 401 Unauthorized
|
|
- 403 Forbidden
|
|
- 404 Not Found
|
|
- 409 Conflict
|
|
- 429 Too Many Requests |