README docs frontend-handout/03-INVOICE-READ-CANCEL.md

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 rowwithout:

  • nested items + addons mapping
  • payments table rebuild
  • mapInvoiceToSyncPayloadShape (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)

  1. Body note set → {note} **canceled by:{username}
  2. Else query re{re} **canceled by:{username}
  3. 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)

  1. Load invoice by uuid + JWT tenant → 404 if missing
  2. Already cancelled → idempotent success
  3. Set status = cancelled, note, updated_at, optional close_by_user_id / is_wastage
  4. Commit DB update
  5. Best-effort side effects (cancel still succeeds if these fail):
    • RabbitMQ inventory.return when returning stock
    • RabbitMQ wallet.cancel when wallet was used
    • RabbitMQ grubtech.status (cancel) — web-application may notify Grubtech
    • Withdrawal rows / product quota updates

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=return or ri=1 when 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