README docs frontend-handout/02-INVOICE-SYNC.md

02 — Invoice sync (POST /v1/invoices/sync)

Create or update invoices (orders). Assigns order_no and customer_invoice_id, then queues persistence.


Endpoint

POST /v1/invoices/sync
Authorization: Bearer <jwt>
Content-Type: application/json

Body: JSON array of invoice objects (not a wrapped object).

Validation: src/validations/invoice.validation.jssyncInvoice
Handler: invoiceController.syncInvoiceinvoiceService.syncInvoice

To cancel, use PUT /v1/invoices/:uuid/cancel — do not rely on sync alone for cancel side effects (inventory / wallet). See 03-INVOICE-READ-CANCEL.md.


Required fields checklist

Per invoice

Field Type
uuid string (client-generated; stable for the order lifetime)
request_id string (trimmed, min length 1) — idempotency key; never leave empty; server never invents one
status "open" | "cancelled" | "completed"
customer_id number
location_id number
user string
created_at number (Unix seconds)
updated_at number (Unix seconds)
subtotal number
vat number
payable number
items array
order_type string (e.g. "dine-in", "takeaway")

Per line item (items[])

Field Type
name string
product_id number
qty number
actual_price number
invoice_uuid string (must match parent uuid)
vat_pct number
customer_id number
location_id number

Full optional catalog: 06-PAYLOAD-REFERENCE.md.


Success response

HTTP 200

{
  "status": true,
  "code": 200,
  "message": "Invoice numbers synchronized successfully",
  "data": [ /* one object per input invoice */ ],
  "trace_id": "<server-trace-id>"
}

What is added/overwritten on each data[] element

Field Type Meaning
order_no number Business-day order number (authoritative; may overwrite client)
customer_invoice_id number Invoice number for this tenant/location flow
cached boolean true if numbers came from Redis / prior resolution; false if newly generated this pass

On idempotent replay of the same request_id, also expect:

Field Type Meaning
request_id_status 0 | 1 | 2 Progress of original request (see below)
error_code string Often "DUPLICATE_REQUEST"

request_id_status

Value Meaning
0 Request accepted earlier; numbers not yet assigned
1 Numbers assigned (worker may still be running)
2 Inventory handoff stage reached for completed invoices

Keep customer_invoice_id / order_no from the response and treat the call as successful (do not create a second UUID).


Critical client behaviour: async persistence

After 200:

  1. Numbers are resolved and cached (Redis, ~24h TTL per request_id / uuid).
  2. Jobs are enqueued (BullMQ order-processing); HTTP does not wait for DB write.
  3. Worker later: validate → refactor → upsert invoice + items + payments.

Implications for mobile:

  • Show order_no / customer_invoice_id immediately from data.
  • If UI needs confirmed DB state, re-fetch GET /history or KDS/dine shortly after.
  • On network retry with the same request_id, expect duplicate-safe response (same numbers), not a second order.

Idempotency rules (request_id)

  1. Generate a unique request_id per user intention (new sync attempt). Retries of the same attempt must reuse the same request_id.
  2. Same request_id in one HTTP batch: later duplicates reuse the first’s numbers and are not queued again (skipQueue stripped from response).
  3. Cross-request: Redis key inv_req_id:{request_id} (24h). Service does not delete it; it expires.
  4. Per-UUID Redis inv-uuid:{uuid} also caches numbers for 24h.

Do not change uuid across retries if you already received numbers for that request_id.


Nested structures to send correctly

Payments

"payments": [
  { "payment_method": "cash", "amount": 50, "tip_amount": 0 },
  { "payment_method": "card", "amount": 55, "tip_amount": 0 }
]

Addons (nest under parent)

"items": [
  {
    "name": "Burger",
    "product_id": 42,
    "qty": 1,
    "actual_price": 25,
    "invoice_uuid": "<same-as-parent-uuid>",
    "vat_pct": 5,
    "customer_id": 1,
    "location_id": 2,
    "linked_uuid": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
    "addons": [
      {
        "name": "Extra Cheese",
        "product_id": 99,
        "qty": 1,
        "actual_price": 2,
        "invoice_uuid": "<same>",
        "vat_pct": 5,
        "customer_id": 1,
        "location_id": 2
      }
    ],
    "modifiers": [
      {
        "quantity": 1,
        "modifier_id": 3930,
        "actual_price": 17,
        "subtotal": 17,
        "name": "Fries + Fanta"
      }
    ]
  }
]

On read APIs, addons are nested again; modifiers return as modifiers arrays.

Notes

"note": "Kitchen: allergy",
"notes": ["No onions", "Extra spicy"]
Field Who sends Storage
note web-pos, iOS invoices.note (string)
notes iOS (array of strings) invoices.order_notes (JSON)

notes is returned as a string array on history/KDS/dine. web-pos currently sends only note; map orderNotenotes: [orderNote] when you adopt the array.

Full attribute lists: 06-PAYLOAD-REFERENCE.md.


Annotated minimal example

[
  {
    "uuid": "550e8400-e29b-41d4-a716-446655440000",
    "request_id": "ios-2026-07-08-op-001",
    "status": "open",
    "customer_id": 437,
    "location_id": 907,
    "user": "[email protected]",
    "user_id": 10,
    "created_at": 1720411200,
    "updated_at": 1720411205,
    "subtotal": 100,
    "vat": 5,
    "payable": 105,
    "order_type": "dine-in",
    "table_id": 5935,
    "is_syspos": 1,
    "is_paid": 0,
    "payments": null,
    "note": "Table 5",
    "notes": ["No onions"],
    "os_type": "ios",
    "app_version": "2.0.0",
    "items": [
      {
        "name": "Burger",
        "product_id": 42,
        "qty": 2,
        "actual_price": 50,
        "price": 50,
        "subtotal": 100,
        "invoice_uuid": "550e8400-e29b-41d4-a716-446655440000",
        "vat_pct": 5,
        "customer_id": 437,
        "location_id": 907,
        "linked_uuid": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
        "is_addon": 0,
        "addons": null,
        "modifiers": []
      }
    ]
  }
]

Completing / paying an order via sync

Typical pattern:

  1. First sync: status: "open", is_paid: 0, payments: null
  2. Later sync same uuid, new request_id (new operation): status: "completed", is_paid: 1, paid_at, payments: [...]

Reuse the same invoice uuid so numbers stay tied to that order. Inventory side effects for completed orders are handled server-side after completion (not for abandoned opens).


Errors

HTTP Cause
400 Joi validation (missing required fields, bad enum, empty request_id, etc.)
400 Service: UUID missing / request_id missing
401 Auth

Validation runs before JWT on this route (validate then jwtAuth).


Client algorithm (suggested)

1. Build invoice DTO locally with new uuid (or existing open uuid)
2. Generate request_id for this sync attempt; persist it until success
3. POST /v1/invoices/sync with [invoice]
4. On 200:
   - Save order_no, customer_invoice_id from data[i]
   - If error_code == DUPLICATE_REQUEST → treat as success with returned numbers
5. On timeout/network error → retry SAME request_id + SAME uuid (do not mint new uuid)
6. Optionally refresh from GET /history or dine/kds

Related