README docs INVOICE_HISTORY.md

Invoice history — list response (GET …/history)

Returns a paginated list of invoices already persisted in the database. Each element in data.orders uses the same per-invoice object shape and datatypes as the POST …/sync request body defined in src/validations/invoice.validation.js:

  • Addon lines (is_addon = 1 in DB) are nested under the parent item’s addons array.
  • Modifiers are returned as modifiers: one flat array of modifier items (no nested items array).
  • payments are rebuilt from the payments table (not the legacy payment_method column).
  • Timestamps are Unix seconds (numbers); boolean flags are 0 / 1.
  • extras on line items is returned as a JSON string ("{\"extraId\": qty}") when present; omitted when empty (history only).
  • DB-only fields (invoice id, item id, internal columns) are omitted.

Authentication: jwtAuth — send a valid JWT in the Authorization header. Tenant scope (customer_id, location_id) is taken from the token, not from query parameters.

Route: GET /v1/invoices/history
Implementation: src/controllers/invoice.controller.jsinvoiceService.getInvoicesInvoiceRepository.getInvoicesWithFiltersmapInvoiceToSyncPayloadShape.

See also: INVOICE_REQUEST.md for the full sync payload field reference.


Authentication

Authorization: Bearer <jwt>

The JWT payload is attached to req.user. The service requires:

Claim Aliases accepted Required
customer_id customerId yes
location_id locationId yes

If either is missing, the API responds with 400 and message: Authentication must include customer_id and location_id.

All queries are automatically scoped to that customer_id + location_id. Sending different tenant ids in query params has no effect.


Query parameters

Param Type Default Description
status string Filter by invoice status: "open", "cancelled", or "completed"
startDate string Inclusive lower bound on invoice_date (repository Op.gte)
endDate string Inclusive upper bound on invoice_date (repository Op.lte)
search string Accepted by the controller but not applied in getInvoicesWithFilters today
page number 1 Page number (1-based)
limit number 10 Page size
sortBy string created_at Column to sort by
sortOrder string DESC ASC or DESC

Status values

Value Meaning
open Order in progress
cancelled Cancelled order
completed Completed / paid order

Matches INVOICE_STATUS in src/enums/invoice.enum.js.

Example request

GET /v1/invoices/history?status=completed&page=1&limit=20&sortBy=created_at&sortOrder=DESC
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response envelope

The handler wraps results in a standard success envelope:

{
  "status": true,
  "message": "Orders history",
  "data": {
    "current_page": "1",
    "records_per_page": 10,
    "total_records": 42,
    "orders": [ /* invoice objects — see below */ ]
  }
}
Field Type Description
status boolean Always true on success
message string "Orders history"
data.current_page string Current page (1-based), stringified
data.records_per_page number Page size
data.total_records number Total matching rows
data.orders array Invoice objects in sync payload shape

Invoice object shape (data.orders[])

Each invoice is mapped by mapInvoiceToSyncPayloadShape (src/utils/invoice-sync-response-mapper.js) so it mirrors the per-invoice element of the sync request schema — same field names, ordering, and datatypes.

Full attribute catalog (including newer columns such as row_discount, crm_delivery_area, rider_arrived_at, item item_status / transfer lists / disposition / cost snapshot fields): see frontend-handout/06-PAYLOAD-REFERENCE.md and INVOICE_REQUEST.md.

Fields included from persistence

Field Type Notes
customer_invoice_id number Coerced from DB
order_no number Order number for the business day
items array Parent line items only; addons nested (see below)
payments array | null From payments table; null when none

Fields not returned (or absent from DB)

Field Notes
request_id Client idempotency key on sync; not stored on the invoice row
cached Sync-only metadata; not returned on history
skipQueue Internal sync flag
request_id_status Idempotent replay metadata from sync only
error_code Idempotent replay metadata from sync only
id (invoice / item) DB primary keys omitted

Columns transformed on read

DB source Response field Transformation
payments table rows payments [{ payment_method, tip_amount, amount }, …] ordered by user slot
modified_by_user_ids (TEXT) modified_by_user_ids Parsed to number[]; defaults to []
order_notes (JSON / TEXT) notes Parsed string array (legacy iOS key); order_notes is not returned
modifier (TEXT, per item) modifiers JSON string → flat array (see Modifiers below)
extras (VARCHAR, per item) extras Normalized map → JSON string; omitted when empty
updated_by_user_ids (TEXT, per item) updated_by_user_ids Parsed to number[]
created_at, updated_at, paid_at, … same keys MySQL/Sequelize dates → Unix seconds (number)
Boolean / flag columns same keys Coerced to 0 / 1

Full field catalog

For invoice-level and line-item fields (finance, flags, CRM, loyalty, etc.), use the same schema as sync — documented in INVOICE_REQUEST.md and enforced in src/validations/invoice.validation.js.

Fields not persisted in MySQL (is_qr_payment, qr_payment_link, is_loyalty_enable, is_sms_enable) are returned with Joi defaults (0 or ""). combo_items is always null (not stored separately).


Line items (items[])

Parent items only

The flat invoice_items rows where is_addon = 1 are removed from the top-level items array. Only parent product lines appear there.

Items are loaded with the invoice, ordered by sort_order ascending.

Addons nested under parent

On sync, nested addons from the client payload are flattened into separate DB rows (is_addon: 1, addon_linked_with = parent linked_uuid). History reverses that:

  1. Shape each row (mapInvoiceItemToSyncShape).
  2. Split into parent rows (is_addon !== 1) and addon rows (is_addon === 1).
  3. For each parent, attach matching addon rows under addons, keyed by addon_linked_with ↔ parent linked_uuid.
  4. Omit extras, is_addon, and addon_linked_with from nested addon objects.
Parent field Value when addons exist
addons array of addon objects, or null if none
is_addon 0
addon_linked_with parent linked_uuid (as stored)

Modifiers (modifiers)

Modifiers stay on the same line item they belong to (parent product or nested addon). Unlike addons, they are not flattened into separate invoice_items rows.

modifiers is a single flat array of modifier item objects — the same field name and structure the client sends on sync. There is no nested items array.

Typical shape

[
  {
    "quantity": 1,
    "id": 21897,
    "subtotal": 17,
    "modifier_id": 3930,
    "actual_price": 17,
    "description": "",
    "priority": 1,
    "name": "Fries + Fanta",
    "comment": "draft"
  }
]

Stored in invoice_items.modifier as a JSON string; returned on GET /history as modifiers (array) with all keys preserved.

Write vs read

Stage Field name Type
POST /sync request (Joi) modifiers flat array | null
Persisted in DB modifier column JSON string
GET /history response modifiers flat JSON array (parsed)

On write, refactorInvoice takes item.modifier ?? item.modifiers, serializes to modifier, and removes modifiers. History renames back to modifiers.

Parsing rules

DB value Response modifiers
"[]" or valid JSON array string flat array
Legacy nested groups ({ items: [...] }) flattened; nested items keys removed
null / "" null

Example — line item with modifiers

{
  "name": "Combo Meal",
  "product_id": 42,
  "qty": 1,
  "actual_price": 25,
  "invoice_uuid": "550e8400-e29b-41d4-a716-446655440000",
  "linked_uuid": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
  "is_addon": 0,
  "modifiers": [
    {
      "quantity": 1,
      "id": 21897,
      "subtotal": 17,
      "modifier_id": 3930,
      "actual_price": 17,
      "name": "Fries + Fanta"
    }
  ],
  "addons": null
}

Differences vs POST /sync

Aspect POST /sync GET /history
HTTP body JSON array of invoices { status, message, data: { …, orders } }
Sync envelope { status, code, message, data, trace_id } { status, message, data } — no code / trace_id
Per-order shape Sync request payload + customer_invoice_id, order_no, cached Same payload shape; no cached
Timestamps Unix seconds (number) Unix seconds (number)
Boolean flags 0 / 1 0 / 1
payments From request body From payments table
Line-item modifiers modifiers modifiers
items Client sends nested addons Server returns nested addons
request_id Required on sync Not returned (not stored)
Persistence Async worker after HTTP response Reads committed DB rows

Full response example

{
  "status": true,
  "message": "Orders history",
  "data": {
    "current_page": "1",
    "records_per_page": 10,
    "total_records": 1,
    "orders": [
      {
        "uuid": "550e8400-e29b-41d4-a716-446655440000",
        "status": "completed",
        "customer_id": 1,
        "location_id": 2,
        "user_id": 10,
        "user": "[email protected]",
        "created_at": 1709985600,
        "updated_at": 1709985900,
        "subtotal": 100,
        "vat": 5,
        "payable": 105,
        "paid": 105,
        "order_type": "dine-in",
        "customer_invoice_id": 1042,
        "order_no": 15,
        "payments": [
          {
            "payment_method": "cash",
            "tip_amount": 0,
            "amount": 105
          }
        ],
        "is_paid": 1,
        "is_syspos": 1,
        "modified_by_user_ids": [10],
        "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",
            "addons": null,
            "is_addon": 0,
            "addon_linked_with": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
            "modifiers": [
              {
                "quantity": 1,
                "id": 21897,
                "subtotal": 17,
                "modifier_id": 3930,
                "actual_price": 17,
                "name": "Fries + Fanta"
              }
            ]
          }
        ]
      }
    ]
  }
}

Errors

HTTP When
401 Missing/invalid/expired JWT
400 JWT missing customer_id or location_id
500 Database/query failure (via ApiError from repository)

What happens on the server

  1. RouteGET /history runs jwtAuth, then invoiceController.getInvoices.
  2. Controller — Picks allowed query keys, calls invoiceService.getInvoices, wraps the result in { status, message, data: { current_page, records_per_page, total_records, orders } }.
  3. Service — Resolves customer_id / location_id from JWT; queries invoices with items; batch-loads payments by invoice UUID; maps each row through mapInvoiceToSyncPayloadShape.
  4. Repository — Builds WHERE (status, tenant, optional invoice_date range); findAndCountAll with InvoiceItem included (as: 'items', ordered by sort_order).

No queue, worker, or Redis involvement on read.


Related endpoint: get by UUID

GET /v1/invoices/:uuid — single invoice, same JWT tenant scope.

Note: This route currently returns the raw repository row without item includes or sync-payload mapping. For a single invoice with the same shape as history, use history with filters or align getInvoiceByUuid with the history mapper in a future change.


Related code

Piece Path
Route src/routes/v1/invoice.route.js
Controller src/controllers/invoice.controller.jsgetInvoices
Service src/services/invoice.service.jsgetInvoices
Response mapper src/utils/invoice-sync-response-mapper.jsmapInvoiceToSyncPayloadShape, nestAddonsIntoParentItems
Payments (read) src/data/repositories/PaymentRepository.jsgetPaymentsByUuids
Modifier normalize (write) src/utils/invoice-refactor.jsserializeModifier, normalizeItem
Validation schema src/validations/invoice.validation.js
Repository (query) src/data/repositories/InvoiceRepository.jsgetInvoicesWithFilters
Sync payload reference docs/INVOICE_REQUEST.md
Auth middleware src/middlewares/jwtAuth.js