README docs INVOICE_CANCEL.md

Invoice cancel (PUT …/cancel)

Cancels a single invoice (order) for the authenticated tenant. Replaces the legacy TRACK API PUT /api/v2/invoice-cancel/:uuid.

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

Route: PUT /v1/invoices/:uuid/cancel
Validation: src/validations/invoice.validation.jscancelInvoice
Implementation: src/controllers/invoice.controller.jsinvoiceService.cancelInvoice

See also:


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

Optional claims used for audit fields:

Claim Used for
username or user Appended to invoice note (**canceled by:{username})
id or user_id close_by_user_id when invoice is open (unless overridden by query)

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

The invoice must belong to the token’s customer_id and location_id. Otherwise the API responds with 404: Invoice not found.


Path parameters

Param Type Description
uuid string Invoice UUID to cancel

Query parameters

Param Type Description
ri 0 | 1 Legacy flag. 1 = return items to inventory (same as type=return).
re string Legacy cancel reason (used when body note is empty).
type wastage | return web-pos disposition. return returns stock; wastage does not. Sets is_wastage on the invoice when wastage.
close_by_user_id number Optional. When invoice status is open, stored as close_by_user_id. Defaults to JWT id / user_id.

Inventory return runs when either:

  • ri=1, or
  • type=return

Wastage flag on the invoice is set when either:

  • body is_wastage=1, or
  • type=wastage

Request body

JSON object (optional; defaults to {}).

Field Type Description
note string Cancel reason shown on the invoice. Combined with **canceled by:{username}.
is_wastage 0 | 1 Legacy wastage material flow flag on the invoice row.

Note format

Matches legacy cancelInvoiceFunction.js:

  1. If body note is set → {note} **canceled by:{username}
  2. Else if query re is set → {re} **canceled by:{username}
  3. Else → **canceled by:{username}

Example requests

web-pos (return to inventory)

PUT /v1/invoices/3878C1D6-1B50-4103-A30D-56ECBFD4BE90/cancel?type=return
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "note": "Customer changed mind"
}

Legacy iOS / TRACK client (ri=1)

PUT /v1/invoices/3878C1D6-1B50-4103-A30D-56ECBFD4BE90/cancel?ri=1&re=Wrong%20order
Authorization: Bearer <jwt>
Content-Type: application/json

{}

Wastage (no inventory return)

PUT /v1/invoices/3878C1D6-1B50-4103-A30D-56ECBFD4BE90/cancel?type=wastage
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "note": "Spoiled before serve"
}

Response

Success — 200

{
  "status": true,
  "message": "Order canceled",
  "data": {
    "uuid": "3878C1D6-1B50-4103-A30D-56ECBFD4BE90",
    "status": "cancelled",
    "message": "Order canceled",
    "return_inventory": true
  }
}
Field Description
data.return_inventory true when an inventory.return message was published
data.already_cancelled Present and true when the invoice was already cancelled (idempotent success)

Errors

HTTP When
400 Validation error, or missing customer_id / location_id in JWT
401 Missing or invalid JWT
404 Invoice not found for tenant scope

Processing flow

Synchronous steps in invoice-services (inside invoiceService.cancelInvoice):

  1. Load invoice by uuid + token customer_id + location_id.
  2. If already cancelled → return success (idempotent).
  3. Build note and set statuscancelled, updated_at → Unix seconds.
  4. If current status is open → set close_by_user_id (query or JWT user).
  5. If wastage → set is_wastage = 1.
  6. DB transaction — update invoices row.
  7. Post-commit side effects (best-effort; errors are logged, cancel still succeeds):
    • Inventory return — publish RabbitMQ inventory.return when ri=1 or type=return.
    • Wallet — publish wallet.cancel when is_wallet_used and crm_user_id > 0.
    • CRM — publish crm.calculate when a valid CRM user is present.
    • Grubtech — publish grubtech.status with action cancel (web-application pushes to Grubtech when location flags allow).
    • Withdrawals — one withdrawal row per payment (refund ledger).
    • Product quota — decrement user_product_sales_counters.sold_qty per line item.

Async consumers in web-application (existing syspos.topic queues):

Routing key Queue binding Handler Behaviour
inventory.return inventory.* inventory-order-return.handler.js Legacy reverse_items_order()mov_type: in via product_materials
wallet.cancel wallet.* wallet-order-cancel.handler.js Legacy wallet transaction (reference_type: invoice-cancel)
grubtech.status grubtech.* grubtech-order-status.handler.js Calls syncGrubtechOrderStatusByUuid (skip gates on location flags / otter_id)

Requires RABBITMQ_URL on web-application workers.


RabbitMQ payloads

Exchange: syspos.topic (see src/core/constant/rabbitmq/syspos.rabbitmq.js).

inventory.returnsyspos.inventory.order_return.v1

Published when returning stock. Built by buildInventoryOrderReturnPayload.

{
  "schema": "syspos.inventory.order_return.v1",
  "occurred_at": "2026-07-04T00:15:52.000Z",
  "invoice_uuid": "3878C1D6-1B50-4103-A30D-56ECBFD4BE90",
  "customer_id": 437,
  "location_id": 907,
  "brand_id": 412,
  "user_id": 4734,
  "customer_invoice_id": 12345,
  "order_no": 42,
  "reference": " Reverse Invoice  3878C1D6-1B50-4103-A30D-56ECBFD4BE90",
  "items": [
    {
      "product_id": 258604,
      "qty": 2,
      "uuid": "line-item-uuid",
      "modifier": "[]",
      "extras": "{}"
    }
  ]
}

wallet.cancelsyspos.wallet.order_cancel.v1

Published when is_wallet_used is set. Built by buildWalletOrderCancelPayload.

{
  "schema": "syspos.wallet.order_cancel.v1",
  "occurred_at": "2026-07-04T00:15:52.000Z",
  "invoice_uuid": "3878C1D6-1B50-4103-A30D-56ECBFD4BE90",
  "uuid": "3878C1D6-1B50-4103-A30D-56ECBFD4BE90",
  "customer_id": 437,
  "location_id": 907,
  "crm_user_id": 991,
  "currency_code": "AED",
  "amount": 55.5,
  "payable": 55.5,
  "transaction_type": "debit",
  "reference_type": "invoice-cancel",
  "reference_id": "3878C1D6-1B50-4103-A30D-56ECBFD4BE90",
  "performed_by_user_id": 4734,
  "source": "invoice-services",
  "description": "Wallet debit on cancellation for invoice 3878C1D6-1B50-4103-A30D-56ECBFD4BE90"
}

Legacy TRACK API mapping

Legacy (web-application) Invoice service
PUT /api/v2/invoice-cancel/:uuid PUT /v1/invoices/:uuid/cancel
?ri=1 ?ri=1 or ?type=return
?re=reason ?re=reason or body note
body note body note
body is_wastage=1 body is_wastage=1 or ?type=wastage

Not ported in invoice-services (still only on legacy TRACK cancel if needed):

  • Material wastage table (updateWastageMaterial / wastage insert)

Ported via RabbitMQ (consumed in web-application):

  • Grubtech order status sync (grubtech.statuscancel)
  • CRM user recalculation (crm.calculate)

Client integration (web-pos)

web-pos calls this endpoint via RTK Query cancelInvoice on invoiceApiSlice:

PUT {NEXT_PUBLIC_INVOICE_SERVICE_URL}/invoices/{uuid}/cancel?type=wastage|return
Body: { "note": "<reason>" }

Source files

Area Path
Route src/routes/v1/invoice.route.js
Controller src/controllers/invoice.controller.jscancelInvoice
Service src/services/invoice.service.jscancelInvoice
Validation src/validations/invoice.validation.jscancelInvoice
Note / withdrawals / quota src/utils/cancelInvoiceHelpers.js
RabbitMQ payloads src/utils/buildInvoiceCancelPayloads.js
Publishers src/queue/publishers/inventory-order-return.publisher.js, wallet-order-cancel.publisher.js, grubtech-order-status.publisher.js
RabbitMQ keys src/core/constant/rabbitmq/syspos.rabbitmq.js