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.js → cancelInvoice
Implementation: src/controllers/invoice.controller.js → invoiceService.cancelInvoice
See also:
- INVOICE_REQUEST.md — sync payload and
statusenum (open|cancelled|completed) - INVOICE_HISTORY.md — list cancelled orders with
?status=cancelled
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, ortype=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:
- If body
noteis set →{note} **canceled by:{username} - Else if query
reis set →{re} **canceled by:{username} - 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):
- Load invoice by
uuid+ tokencustomer_id+location_id. - If already
cancelled→ return success (idempotent). - Build
noteand setstatus→cancelled,updated_at→ Unix seconds. - If current status is
open→ setclose_by_user_id(query or JWT user). - If wastage → set
is_wastage = 1. - DB transaction — update
invoicesrow. - Post-commit side effects (best-effort; errors are logged, cancel still succeeds):
- Inventory return — publish RabbitMQ
inventory.returnwhenri=1ortype=return. - Wallet — publish
wallet.cancelwhenis_wallet_usedandcrm_user_id > 0. - CRM — publish
crm.calculatewhen a valid CRM user is present. - Grubtech — publish
grubtech.statuswith actioncancel(web-application pushes to Grubtech when location flags allow). - Withdrawals — one
withdrawalrow per payment (refund ledger). - Product quota — decrement
user_product_sales_counters.sold_qtyper line item.
- Inventory return — publish RabbitMQ
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.return — syspos.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.cancel — syspos.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/wastageinsert)
Ported via RabbitMQ (consumed in web-application):
- Grubtech order status sync (
grubtech.status→cancel) - 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.js → cancelInvoice |
| Service | src/services/invoice.service.js → cancelInvoice |
| Validation | src/validations/invoice.validation.js → cancelInvoice |
| 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 |