126 lines
6.6 KiB
Markdown
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 |