Files
kooltab-backoffice/docs/api.md
T
brammie15 1e4438ec5a
CI / build-and-test (push) Canceled after 0s
feat: add stuff
2026-10-20 14:45:22 +02:00

126 lines
6.6 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` - List devices in venue
### Devices
`POST /devices/register` - Register a new device
`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)
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.
### 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` - Create a command
`GET /venues/:venueId/commands` - List command history
`POST /venues/:venueId/commands/devices/:deviceId/revoke` - Revoke device (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