03 — Invoice history, get-by-UUID, cancel
GET /v1/invoices/history
Paginated list of persisted invoices. Each order uses the same sync payload shape as POST /sync (minus request_id / cached).
GET /v1/invoices/history?status=completed&page=1&limit=20&sortBy=created_at&sortOrder=DESC
Authorization: Bearer <jwt>
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
status |
string | — | "open" | "cancelled" | "completed" |
startDate |
string | — | Inclusive lower bound on invoice_date |
endDate |
string | — | Inclusive upper bound on invoice_date |
search |
string | — | Accepted but not applied in repository today |
page |
number | 1 |
1-based page |
limit |
number | 10 |
Page size |
sortBy |
string | created_at |
Sort column |
sortOrder |
string | DESC |
ASC or DESC |
Tenant: from JWT only.
Success response
{
"status": true,
"message": "Orders history",
"data": {
"current_page": "1",
"records_per_page": 10,
"total_records": 42,
"orders": [ /* sync-shaped invoices */ ]
}
}
| Field | Type | Notes |
|---|---|---|
data.current_page |
string | Stringified page number |
data.records_per_page |
number | |
data.total_records |
number | |
data.orders |
array | Sync-shaped invoices |
Per-order shape highlights (vs sync)
| Concern | Behaviour |
|---|---|
| Addons | Nested under parent addons[]; top-level list has parents only |
| Modifiers | modifiers flat array |
| Payments | From payments table → [{ payment_method, tip_amount, amount }] or null |
| Timestamps | Unix seconds |
| Flags | 0 / 1 |
notes |
String array (from DB order_notes) |
extras on item |
JSON string when present; omitted when empty |
| Not returned | request_id, cached, DB primary ids |
Full field list: 06-PAYLOAD-REFERENCE.md.
Example order element
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"customer_id": 1,
"location_id": 2,
"user": "[email protected]",
"created_at": 1709985600,
"updated_at": 1709985900,
"subtotal": 100,
"vat": 5,
"payable": 105,
"order_type": "dine-in",
"customer_invoice_id": 1042,
"order_no": 15,
"is_paid": 1,
"payments": [
{ "payment_method": "cash", "tip_amount": 0, "amount": 105 }
],
"notes": ["No onions"],
"items": [
{
"name": "Burger",
"product_id": 42,
"qty": 1,
"actual_price": 5.5,
"invoice_uuid": "550e8400-e29b-41d4-a716-446655440000",
"customer_id": 1,
"location_id": 2,
"vat_pct": 5,
"linked_uuid": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"is_addon": 0,
"addons": null,
"modifiers": [
{
"quantity": 1,
"id": 21897,
"subtotal": 17,
"modifier_id": 3930,
"actual_price": 17,
"name": "Fries + Fanta"
}
]
}
]
}
Errors
| HTTP | When |
|---|---|
401 |
Bad JWT |
400 |
JWT missing customer_id / location_id |
500 |
Query failure |
No queue/Redis on this path — reads committed DB rows.
GET /v1/invoices/:uuid
GET /v1/invoices/{uuid}
Authorization: Bearer <jwt>
Returns a single invoice scoped to JWT tenant.
Important limitation for mobile
Today this returns the raw repository / Sequelize row — without:
- nested items + addons mapping
paymentstable rebuildmapInvoiceToSyncPayloadShape(sync DTO)
So it is not the same shape as history/KDS/dine.
Recommendation for Android/iOS: Prefer GET /history (filter/list locally or by status) until this endpoint is aligned to the sync mapper. Treat GET /:uuid as low-level / transitional.
Errors
| HTTP | When |
|---|---|
400 |
Missing tenant claims |
404 |
Invoice not found |
401 |
Auth |
PUT /v1/invoices/:uuid/cancel
Cancels one invoice for the authenticated tenant. Replaces legacy PUT /api/v2/invoice-cancel/:uuid.
PUT /v1/invoices/{uuid}/cancel?type=return
Authorization: Bearer <jwt>
Content-Type: application/json
{
"note": "Customer changed mind"
}
Path
| Param | Description |
|---|---|
uuid |
Invoice UUID |
Query parameters
| Param | Type | Description |
|---|---|---|
ri |
0 | 1 |
Legacy: 1 = return items to inventory |
re |
string | Legacy reason if body note empty |
type |
"wastage" | "return" |
return → inventory return; wastage → set wastage flag, no return |
close_by_user_id |
number | Optional; when invoice was open, stored as closer |
Inventory return when ri=1 or type=return.
Wastage when body is_wastage=1 or type=wastage.
Body (optional; default {})
| Field | Type | Description |
|---|---|---|
note |
string | Cancel reason |
is_wastage |
0 | 1 |
Wastage flag |
Note composition (server)
- Body
noteset →{note} **canceled by:{username} - Else query
re→{re} **canceled by:{username} - Else →
**canceled by:{username}
Success response
{
"status": true,
"message": "Order canceled",
"data": {
"uuid": "3878C1D6-1B50-4103-A30D-56ECBFD4BE90",
"status": "cancelled",
"message": "Order canceled",
"return_inventory": true
}
}
| Field | Meaning |
|---|---|
data.return_inventory |
true if inventory return message was published |
data.already_cancelled |
true if already cancelled (idempotent success) |
Example — iOS / TRACK style
PUT /v1/invoices/3878C1D6-1B50-4103-A30D-56ECBFD4BE90/cancel?ri=1&re=Wrong%20order
Authorization: Bearer <jwt>
Content-Type: application/json
{}
Example — wastage (no stock return)
PUT /v1/invoices/3878C1D6-1B50-4103-A30D-56ECBFD4BE90/cancel?type=wastage
Authorization: Bearer <jwt>
Content-Type: application/json
{ "note": "Spoiled before serve" }
What the server does (summary)
- Load invoice by uuid + JWT tenant → 404 if missing
- Already
cancelled→ idempotent success - Set
status = cancelled, note,updated_at, optionalclose_by_user_id/is_wastage - Commit DB update
- Best-effort side effects (cancel still succeeds if these fail):
- RabbitMQ
inventory.returnwhen returning stock - RabbitMQ
wallet.cancelwhen wallet was used - RabbitMQ
grubtech.status(cancel) — web-application may notify Grubtech - Withdrawal rows / product quota updates
- RabbitMQ
Mobile clients only need to call this HTTP API; they do not publish RabbitMQ.
Errors
| HTTP | When |
|---|---|
400 |
Validation / missing tenant claims |
401 |
Auth |
404 |
Invoice not found for tenant |
Mobile checklist
- Prefer cancel endpoint over syncing
status: "cancelled"if you need inventory/wallet side effects - Use
type=returnorri=1when stock should return - Treat second cancel as success (
already_cancelled) - Refresh local UI / history after cancel
Sync vs history vs cancel (quick matrix)
| Action | Endpoint |
|---|---|
| Create / update open or paid order | POST /sync |
| List past orders | GET /history |
| Cancel + side effects | PUT /:uuid/cancel |
| Strong single-order sync-shaped read | Use history (or KDS/dine) until GET /:uuid is mapped |