Plain-English summary
"Can I undo this sale?" has three different answers depending on when you ask. Before the order is closed, the cashier can remove items (an item void) or cancel the whole order (cancellation) right from the counter — kitchen gets notified, the dine-in table frees up, nothing hits the till. After the order is closed and paid, the counter POS deliberately has no undo button: money came in, so reversing it is a back-office action — a Refund recorded against the original payment in the web admin, which then subtracts from the register session's expected cash and shows up in the Refund History report. There is no "void a closed order" button at the counter as of this writing.
When you'd use this
- A customer changes their mind about one dish after the order was accepted → item void (edit the order, remove the line)
- A whole table walks out / an order was rung up by mistake → cancel order (pre-close)
- A customer brings food back after paying, or was double-charged → refund (back-office, against the original payment)
- A payment was recorded with the wrong method or amount → edit the payment (counter) or void the payment (back-office) and re-record
- A formal invoice needs reversing for an account customer → credit note (back-office)
The decision table
| Situation | Action | Where | Who |
|---|---|---|---|
| Order not yet closed, remove one item | Edit order → delete line (item void) | Counter POS | Any cashier (the former Restricted-Waiter block is disabled on the current build — see role gates) |
| Order not yet closed, scrap everything | ⋮ menu → Cancel Order (+ optional reason) | Counter POS | Requires the SalesOrders.Delete role (server-checked); blocked if order is locked |
| Order closed & paid, money must go back | Refund against the original payment | Admin app (admin.foodops.io / finance-v2) → Payments | Back-office role (payments/invoices permissions) |
| Payment recorded wrongly (still open session) | Edit payment from the order's payment list | Counter POS | Cashier with create permission |
| Payment must be struck entirely | Void payment | Admin app / sales API | Invoices.AddEdit role |
| Account/invoice customer needs a reversal document | Credit note + allocation | Admin app (back-office) | Invoices role |
Walkthrough — cancel an order (pre-close, at the counter)
- Open the order from the Counter status tabs (any non-closed state).
- Click Open order details (the icon button on the order card) to open the right-side details panel, then click the More options button (horizontal-dots, bottom-right next to Print) → Cancel Order (shown in red). The option only appears while the order is not locked, not already cancelled, and not completed/closed
[code: foodops.counter.app/src/components/orders/OrderDetailsPanel.tsx — showMoreMenu + canCancelOrder() @ 32669a9]. - A confirmation dialog warns "This action cannot be undone" and offers a Cancel Remarks text box. The reason is optional in the current build (the dialog labels it "Optional") — it is stored on the order as
CancelRemark. - Confirm. What happens server-side
[code: hubits-api-sales/Hubits.Sales.Api/Controllers/OrdersController.cs — POST /api/orders/cancel @ f26ba1f]:- The server first checks the caller holds the
SalesOrders.Deleterole, read directly from org-scopedUserRoles(not the cachedUser.IsInRole); lacking it, the request is rejected with HTTP 403 and "You do not have permission to cancel orders" before any state changes are made. - Order status → Cancelled, status-change timestamped.
- If dine-in, the table is freed (set back to Vacant).
- All open KOTs and their items are flagged cancelled, and cancellation slips print to each affected kitchen (one slip per kitchen per KOT) naming who cancelled and the reason.
- The order moves to the Cancelled tab on the counter and the KDS.
- The server first checks the caller holds the
- Works offline: the counter queues the cancellation locally and syncs it when connectivity returns
[code: foodops.counter.app/src/offline/actions/orderActions.ts].
Walkthrough — void an item (remove a line after acceptance)
- Tap the Edit order button (pencil icon) on the order card to go straight to the Edit Order page — or: Open order details → More options → Edit Order. Both reach the same screen (available while the order is not locked, cancelled, paid, or closed).
- Delete the line (or reduce its quantity) and save.
- Server-side, removed lines are not deleted — they are marked voided with a timestamp, and any reason you gave is kept on the line (
IsVoided,VoidedTime,CancelledComments). The change log records "Cancelled items: …"[code: hubits-api-sales/Hubits.Sales.Api/Services/Counter/Orders/OrderUpdateService.cs:665-690 @ 20fc7ca]. - The kitchen is notified — cancelled KOT items trigger the same per-kitchen cancellation-slip mechanism as full cancellation.
Role gate: Restricted Waiter — NOT enforced on the current stack
There was a "Restricted Waiter" gate that rejected line removals / quantity-decreases on a non-draft order with "Your role does not allow removing line items or decreasing quantities." — but that enforcement was DISABLED on 2026-06-17 and remains off on the launch build. The code still detects a restricted-waiter removal/decrease and writes an audit log line, but the blocking return (false, …) is commented out, so the save proceeds for everyone [code: hubits-api-sales/Hubits.Sales.Api/Services/Counter/Orders/OrderUpdateService.cs:671-684 — "Restricted Waiter removal/decrease restriction DISABLED 2026-06-17 … the save is no longer blocked"; the return (false, "Your role does not allow…") at :682 is commented out @ main]. The reason given in-code: the permission isn't granted to any client and the gate was tripping legitimate offline edits replayed on sync. Do not tell users that a Restricted Waiter is blocked from removing items — they are not, on the current build. (To re-enable, the commented return would be restored.)
Refunds — what exists after close
There is no refund button on the counter POS. The counter's RefundManager.tsx is an empty 13-line stub and PaymentsPage.tsx is a placeholder shell (re-verified 2026-06-29 against foodops.counter.app@32669a9). Once an order is Closed, the counter's cancel/edit options disappear by design.
The shipped refund mechanism is in the back-office (admin) app — admin.foodops.io / finance-v2.hubits.io, built from foodops-admin-app. (The legacy app.foodops.io payments web app, where this flow previously lived, is retired at the July-10 launch — it is no longer the surface.) Payment management — payment details, void-and-recreate invoice edits, and the payments/refund reports — lives there [code: foodops-admin-app/apps/admin/src/pages/sales/PaymentDetailsPage.tsx; apps/admin/src/components/sales/PaymentFormDialog.tsx; apps/admin/src/components/sales/VoidInvoiceDialog.tsx; apps/admin/src/components/sales-receipts/SalesReceiptHeaderActions.tsx @ main].
Backend behaviour (in hubits-api-sales, unchanged by the surface move):
- A refund is stored as a
PaymentType = Refundpayment, linked to the source payment viaPaymentLinkedRefund, and surfaces in the Refund payment-list filter[code: hubits-api-sales/Hubits.Sales.Api/Services/Payments/PaymentsHelper.cs:151,283 PaymentType.Refund @ main]. - Allocation/refund amounts are capped at the target invoice's outstanding balance ("Payment amount is greater than the invoice outstanding balance.")
[code: PaymentsHelper.cs:530,661 @ main]. (Residual risk: the exact legacy "Refund amount is greater than the payment outstanding balance." string was not re-located in currentPaymentsHelper— the cap behaviour holds but the precise message/cap basis for a refund-vs-original-payment should be re-confirmed once the admin refund UI is walked.) - The refund is money out of the drawer/account: it reduces the register session's expected cash (see below).
Note: the new admin app's payment-management UI is still thin —
PaymentDetailsPage.tsxdoes not yet expose a Delete/Void/Edit control andsalesPaymentServicehas nodelete()/void()(known admin gap), even though the backendPOST /payments/{id}/voidendpoint exists. Treat the admin refund/void UI as not-fully-walked.
Card payments: there is no payment-gateway reversal integration in this flow — a "card" refund is an accounting record; the actual card reversal happens outside Foodops (terminal/bank).
Payment void (back-office)
A payment can also be voided outright — POST /payments/{id}/void, gated to the Invoices.AddEdit role; it unallocates the payment first, then voids it. Voiding flips the payment status to PaymentVoid and refreshes the customer balance; voided payments are excluded from sales and reconciliation maths [code: hubits-api-sales/Hubits.Sales.Api/Controllers/PaymentsController.cs:739-743 VoidPayment [Authorize(Roles=Invoices.AddEdit)], unallocate-then-void :749-780 @ main]. Use void for "this payment record should never have existed"; use refund for "real money went back to the customer".
Credit notes
For invoiced/account customers, the formal reversal document is a credit note (create, allocate against invoices, unallocate) — back-office only (CreditNotesController; the admin app shows credit-note tabs on sales receipts). Notably, the sales API's refund-history report is credit-note based — it queries Invoices of TransactionType.CreditNote ("credit notes issued") for a date range [code: hubits-api-sales/Hubits.Sales.Api/Controllers/ReportsController.cs:504-526 GetRefundHistory @ main].
Register / session impact
- Expected cash math: at session close, per currency,
expected = opening float + cash payments − cash refunds. Refund-type payments recorded during the session directly reduce what the drawer should contain[code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:3683-3693, 4042-4102 @ 20fc7ca]. - Day Summary carries a refunds total (refund-type payments, excluding voided ones) and nets it against payments
[code: .../Services/Counter/Reports/DaySummaryReportService.cs:303-311 @ 20fc7ca]. - Cancelled orders never reach the till — no payment exists, so reconciliation is untouched; they simply show in the Cancelled tab and order logs.
- Voided items reduce the order total before payment, so they're invisible to reconciliation too (the audit trail lives on the order's line items and logs).
Reporting
| Report | Surface | What it shows |
|---|---|---|
| Refund History | Admin app (RefundHistoryReportPage) → sales API GetRefundHistory |
Credit notes issued over a date range |
| Voids & Comps | Admin app (VoidsAndCompsReportPage) |
Voided items and FOC/comped orders |
| Day Summary | Counter + admin | Refunds total netted against the day's payments |
| Order logs | Counter, per order (/{orderId}/logs) |
Who cancelled/edited what, when, with remarks |
Key concepts
- Item void — a line removed from a not-yet-closed order. Soft-flagged (
IsVoided), never hard-deleted; keeps a timestamp and reason; triggers kitchen cancellation slips. - Order cancellation — the whole order struck before payment/close. Frees the table, cancels KOTs, prints cancellation slips, keeps the order in history under Cancelled.
- Refund — a payment-type record (PaymentType = Refund) linked to the original payment, capped at that payment's unapplied balance. Money out; reduces expected cash.
- Payment void — administrative strike of a payment record (status PaymentVoid). Not a money movement; excluded from totals.
- Credit note — the document-level reversal for invoiced customers; allocatable against invoices.
- Restricted Waiter — a role that historically could add but not remove/decrease items on accepted orders. The enforcement is disabled on the current stack (since 2026-06-17); the role no longer blocks removals — only an audit log line is written.
- Locked order — a locked order can't be cancelled or edited until unlocked (toggle-lock is on the same ⋮ menu).
Common questions
Q: Can a cashier refund a closed order at the counter? A: No. Once Closed, the counter offers no cancel/void/refund action. Refunds are recorded in the back-office payments module against the original payment. This is by design: post-close reversals are money movements, and they're kept off the front counter.
Q: Is a cancellation reason required? A: No — the Cancel Remarks field is optional in the current build. Whatever you enter is stored on the order and printed on the kitchen cancellation slip.
Q: Does cancelling an order need a manager PIN? A: No PIN/approval flow exists in the current code. The only gate is coarse: the action simply isn't offered on locked or closed orders. (The Restricted-Waiter removal block, which once added a per-role gate, is disabled on the current build — see the role-gate note. US-POS-011 envisions manager approval for void/refund — that remains aspirational.)
Q: What happens on the KDS when I void an item or cancel an order? A: KOT items are flagged cancelled and each affected kitchen gets a printed cancellation slip naming the canceller and reason; the KDS moves the ticket to its Cancelled tab (an "Order cancelled" toast appears on the dedicated KDS app).
Q: Customer paid cash and returns the food 10 minutes after close — exact steps?
A: Admin app (admin.foodops.io / finance-v2): Payments → find the payment for that order → Refund → enter the amount (≤ the original payment) → save. Give the cash from the drawer. At session close, expected cash is automatically lower by that amount, so the drawer reconciles. (The old app.foodops.io payments page is retired at launch.)
Q: Can I refund more than the original payment (e.g., goodwill extra)? A: No — validation caps the refund at the original payment's outstanding/unapplied balance. A goodwill payout is a different transaction (e.g., an expense), not a refund.
Q: How do I undo a payment that was keyed wrong (right order, wrong method)?
A: While the session is live, use the payment's edit option in the order details panel (EditPaymentModal / update-payment). If the record is fundamentally wrong, a back-office user with Invoices.AddEdit can void it and re-record.
Q: Do voided items restock inventory?
A: The void is recorded on the sales side (IsVoided); recipe/stock reversal behaviour is not verified in this pass — treat US-POS-EDGE-007's "stock deduction reverses" as unconfirmed.
Q: Where do I see who cancelled an order and why? A: Order logs (per-order, from the counter) record the cancellation with user and remarks; the Cancelled tab lists all cancelled orders; Voids & Comps aggregates item voids for managers.
Q: Does a refund show on the Z-report / session reconciliation? A: Yes — refunds recorded during the session subtract from expected cash per currency and appear in the Day Summary's refunds total.
Q: Is there a "sales return" document like Shops has?
A: Not on the FoodOps counter. The Shops retail POS has a sales-returns surface (see Shops Sales Returns); FoodOps handles the equivalent via refunds + credit notes. A SalesReturnResourceStrategy exists in the sales API's sub-resource plumbing, but no FoodOps counter UI exposes sales returns.
Q: Can an order be cancelled after it's paid but before it's closed?
A: Edge case: canCancelOrder() blocks completed/closed orders but not paid-and-still-open ones, and the server cancel endpoint only rejects already-cancelled orders. Cancelling a paid order does NOT auto-refund the payment — the payment would need separate back-office handling. Avoid this path; close the order, then refund properly.
Edge cases and known issues
Cancel succeeds but no cancellation slip prints
- Cause: cancellation-slip printing is fired best-effort after the save; printer/print-hub errors are logged, not surfaced.
- Workaround: tell the kitchen verbally; check printer status; the KDS Cancelled tab is the reliable signal.
Refund recorded against the wrong payment
- The refund is linked (
PaymentLinkedRefund) to whichever payment you opened. Void the refund payment (back-office) and re-record against the correct one.
Offline cancellation conflicts
- An order cancelled offline on one terminal while another terminal edits it can produce a conflict entry (see the counter's Conflicts page). Resolve from the Conflicts screen; the cancellation generally wins since the server marks status Cancelled idempotently.
Restricted Waiter removals (gate disabled)
- On older builds the server rejected a Restricted Waiter's line removal at save time. That enforcement is off on the current stack — the removal now succeeds and only an audit log line is recorded (
OrderUpdateService.cs:671-684 @ main). No user-facing error appears.
Honest gaps (what does NOT exist, verified 2026-06-11)
- No counter-POS refund button or post-close void (
RefundManager.tsxis an empty stub). - No manager-PIN / approval workflow for voids, cancellations, or refunds.
- No payment-gateway (card) reversal integration in these repos.
- No FoodOps "sales return" document; nearest equivalents are refund payments and credit notes.
Related
POS Takeaway Order — payments, Pay & Close, payment-edit flow
Session Reconciliation and Cash Variance — where refunds land at close
Z-Report, Reports Catalogue — Refund History, Voids & Comps, Day Summary
Customers & CRM — credit notes, customer balances
Shops Sales Returns — the retail-side equivalent
User Stories: POS — US-POS-003 (cancel dine-in), US-POS-011 (void/refund), US-POS-EDGE-007 (cancel items)
2026-08-08 — codebase (haafai/hubits-api-sales bb69312..cdb3b32). No contradiction of this entry's void/refund/cancel claims. One item adjacent to the entry's Register/session-impact section, not a correction: DaySummaryReportService.cs's register-session shaping (ADR-0011) stopped subtracting the report-period's total cash withdrawals from every session row's base-currency-cash ExpectedAmount — that double-deduction (the stored ExpectedAmount already nets its own session's withdrawals, and on a multi-session day the period-wide withdrawal total was being applied to each session) was making the Day/Session Summary's displayed Expected Cash wrong on multi-session days. This is a withdrawal-driven bug, not the entry's documented refund-driven expected-cash formula (SessionsController.cs, opening float + cash payments − cash refunds), so no existing claim here is contradicted — but it's the same 'Expected Cash' concept the entry's Q&A ('Does a refund show on the Z-report / session reconciliation?') touches, so worth a mention once this report is live-walked, and more directly relevant to the session-reconciliation entry. The OrderUpdateService.cs bomReversals/IsBomStockDeducted hunk is unchanged from the already-flagged 2026-08-07 history entry (still cut off before the branch body or consumption site) — does not newly resolve the open 'Do voided items restock inventory?' question; no action needed yet. Unrelated, out of this entry's void/refund/cancel scope: outlet-specific reference-number resolution extended further (ResolvePaymentOutletIdAsync/ResolveCreditNoteOutletIdAsync, already flagged 2026-08-02); a SignalR OrderEventPublisher 'routing envelope' refactor completing across OrdersController/OrderUpdateService/PaymentsController (OrderAdded/OrderUpdated/OrderStatusUpdated/KitchenOrderAdded — pure real-time-notification plumbing; no CancelOrder hunk appears in this diff, so the SalesOrders.Delete role-gate and Restricted-Waiter claims stand unchallenged); GetPaymentsReceived gaining a 'Filter by Modified Date' option and ModifiedDate column, plus a new GET /reports/export-columns endpoint (a report already noted 2026-07-22 as outside this entry's Reporting table); PaymentsController.MapPaymentResult adding PaymentTypeId and (already-noted 2026-08-02) actually projecting LinkedRefunds; PaymentsHelper.Create's refund branch continuing the already-documented (2026-08-02) UpdateTotals/BaseAmount fixes with an added null-guard on LinkedRefunds. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-08-07 — codebase (haafai/hubits-api-sales bb69312..14fe122, OrderUpdateService.cs UpdateOrderAsync). POSSIBLE NEW BEHAVIOR bearing directly on this entry's open 'Do voided items restock inventory?' question (currently answered 'unconfirmed' / US-POS-EDGE-007 flagged unresolved): the item-diff loop that soft-flags a removed/reduced line (
existingItem.IsVoided = true; existingItem.VoidedTime = ...) now checksexistingItem.IsBomStockDeductedimmediately after, alongside a newbomReversalsaccumulator declared earlier in the method with the comment '(lineItemId, quantityToReverse, clearFlag) — BOM stock to restore for removed/reduced prepared lines; applied after the diff, before save.' This strongly suggests line-level item voids (not full order cancellation, which already reverses BOM stock separately per the 2026-07-04 history entry onReverseBomStockDeduction()) are gaining their own recipe/stock-reversal path. However, the diff hunk is cut off before showing what theif (existingItem.IsBomStockDeducted)branch actually does or how/wherebomReversalsis consumed and applied — so this does NOT yet confirm the reversal is real, complete, or correctly scoped (partial quantity decreases vs full removal, multi-line orders, etc.). Do not update the 'Do voided items restock inventory?' answer yet; instead flag for the orchestrator to pull the full OrderUpdateService.cs diff/method and, ideally, live-verify against hubits-api-inventory before folding this into the Common Questions / Honest Gaps sections. Unrelated in the same diff, out of this entry's void/refund/cancel scope: outlet-specific reference-number resolution extended to Payments/CreditNotes (ResolvePaymentOutletIdAsync/ResolveCreditNoteOutletIdAsync/refundOutletId — already flagged out-of-scope in the 2026-08-02 history entry), the SessionsController close-summary/close-summary-pdf endpoints and PosDefaultViewMode/SoftEnforcePaymentReference session fields (already partly noted 2026-08-02; the latter two are net-new and concern POS view defaults and Transfer-payment reference enforcement, not void/refund/cancel), an OpenedTime UTC-serialization bug fix, a further SignalR OrderEventPublisher/routing-envelope refactor across OrdersController/OrderUpdateService (pure real-time-notification plumbing, does not touch the cited CancelOrder role-gate or Restricted-Waiter code — no CancelOrder hunk appears in this diff at all, so the SalesOrders.Delete role-gate claim stands unchallenged), and GetPaymentsReceived / export-columns additions to a report (Payments Received) that remains outside this entry's Reporting table. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-08-04 — codebase (haafai/foodops.counter.app f52b6dc..5167f45, OrderDetailsPanel.tsx + new src/offline/actions/voidToBin.ts). NEW BEHAVIOR, additive to this entry's 'cancel an order (pre-close)' walkthrough, not yet a contradiction of the documented default flow: a new session-scoped flag (
enableVoidBin, read viagetCurrentSession()) gates an alternate Cancel Order path. When the flag is on, clicking Cancel Order in the order-details panel no longer opens the documented confirmation dialog ('This action cannot be undone' + optional Cancel Remarks textbox) — it calls a newhandleVoidBinCancel()which immediately invokesvoidOrderToBin(...)and shows a toast ('Order {orderNumber} moved to the void bin.'), with the cancellation reason instead captured via a new persistent, stacking snackbar mounted app-wide (per the commit series: 'void bin offline plumbing — enableVoidBin flag + Dexie v26 voidBinEntries + sync actions', 'void-to-bin engine + immediate cancel wiring', 'void bin reason-capture snackbar', 'void bin discard buttons + read-only panel + live badge on dine-in'). This looks like a in-progress 'Void Bin' feature (offline-first, with its own Dexie table, sync actions, a read-only panel, discard buttons, and a live badge on dine-in) that may eventually replace or sit alongside the plain Cancelled-tab flow this entry documents. Because the flag defaults tofalsein local state until the session loads and its production rollout state is unknown from this diff alone, the entry's existing default walkthrough (confirmation dialog, optional Cancel Remarks) is not contradicted — but this is a strong flag for the orchestrator to check whetherenableVoidBinis live for any real sessions, and if so, live-walk the void-bin UI (snackbar reason capture, discard/restore behavior, the dine-in badge, and whether voided-to-bin orders still land in the same 'Cancelled' tab/KDS state this entry describes) before folding it into the walkthrough and decision table. Unrelated in the same diff, out of this entry's void/refund/cancel scope:orderActions.ts'supdateOrdernow also threadsproductVariantIdinto the offline item-update payload (two call sites) — a data-completeness fix for offline order-line sync, not a behavior change to item-void or cancellation semantics. (auto-applied by the truth pipeline; adversarially gated)2026-08-02 — codebase (haafai/foodops-admin-app 9856e01..76df568, PaymentDetailsPage.tsx). Additive detail beyond the same-day 46a456e verification note already in this history (which covered the new canRefund gate, RefundPaymentDialog, and Refunds/linkedRefunds table): the payment-detail header now also renders a
detail.isRefundamber 'Refund' badge alongside the existing Paid/Voided status badges, so a payment record that is itself a refund is visually distinguishable on sight from the payment it refunds. This is a small UI complement to the entry's already-documented 'PaymentType = Refund' backend claim (PaymentsHelper.cs:151,283) and to the already-noted Refund action/table — it does not contradict anything and needs no correction, just a mention once the admin refund UI is live-walked. Unrelated in the same diff, out of this entry's void/refund/cancel scope: RefundHistoryReportPage.tsx and VoidsAndCompsReportPage.tsx were refactored onto a shared quick-date-range-preset picker (newReportDatePresetscomponent /useReportDatePresetshook, replacing ad hoc startOfDay/endOfDay helpers withpresetRange/ymdToStartOfDay/ymdToEndOfDay), and their export calls now pass acolumnsparam. This changes how the date filter is picked and what an export includes, not what the two reports show — the entry's Reporting-table descriptions of Refund History (credit-note based) and Voids & Comps stand unchanged. (auto-applied by the truth pipeline; adversarially gated)2026-08-02 — codebase (haafai/hubits-api-sales bb69312..1b4f871). NEW BEHAVIOR, additive to this entry's Register/session-impact and refund coverage, no contradiction of existing claims: (1) SessionsController gained two new endpoints, GET /sessions/{id}/close-summary and .../close-summary/pdf, which build a 'Session Summary' thermal-receipt DTO/PDF by reusing DaySummaryReportService — so its figures reconcile with the already-documented Day Summary refunds total. Worth a line in the Register/session impact section and the Reporting table once the counter/admin UI surface for this new close-summary slip is identified and live-walked. (2) A payment-detail bug fix: PaymentsController's MapPaymentResult LinkedRefunds field was previously hardcoded to an empty list ('the admin Related tab never showed refunds'); it now actually projects payment.LinkedRefunds (id, refund payment id, amount, reference, date, status), so the admin payment-detail page's Related tab will start showing refunds issued against a payment — relevant to the entry's 'Refund recorded against the wrong payment' edge case, which already describes the PaymentLinkedRefund link but not its UI visibility. (3) A refund-integrity bug fix in PaymentsHelper.Create's refund branch: after linking a refund payment to its original via PaymentLinkedRefund, the code now calls refundedPayment.UpdateTotals(context) so the original payment's stored UnappliedAmount actually decreases; per the added comment, without this fix 'the refunded balance stays stale and the guard above ... would let the same money be refunded repeatedly' — this hardens (does not contradict) the entry's existing claim that refund/allocation amounts are capped at the outstanding balance (PaymentsHelper.cs:530,661), closing a staleness gap in that same cap mechanism. A related currency-conversion fix (BaseAmount operator-precedence bug, F5) was also fixed in the same block but only affects an internal ledger field, not a documented user-facing claim. Unrelated in the same diff, out of this entry's scope: outlet-specific reference-number resolution added to CreditNotesController/PaymentsController/PaymentsHelper (ResolvePaymentOutletIdAsync / ResolveCreditNoteOutletIdAsync) — this changes which document-number sequence/prefix a payment or credit note draws from, not any void/refund/cancel semantics this entry documents; and a SignalR broadcast refactor across OrdersController/OrderUpdateService (new IOrderEventPublisher, 'routing envelope' scoping) that is pure real-time-notification plumbing and does not touch the cited cancel-role-gate, item-void, or Restricted-Waiter code paths. (auto-applied by the truth pipeline; adversarially gated)
2026-08-02 — codebase (haafai/foodops-admin-app 9856e01..46a456e, PaymentDetailsPage.tsx; commit 'Add unalocatted amount refund'). NEW BEHAVIOR: the admin app's Payment Details page now has a direct Refund action, closing part of the gap this entry's 'admin refund/void UI is thin' note flagged. A new
canRefundgate (!!detail.customerId && detail.unappliedAmount > 0.005 && !detail.isVoided) shows a 'Refund' item (RotateCcw icon) in the payment's dropdown menu, alongside the existing Edit Payment / Remove items, opening a newRefundPaymentDialogcomponent. The page also now renders a 'Refunds' table listing anydetail.linkedRefundsissued against that payment (reference, date, status, amount), with each row linking through to the refund's own payment record viarefundPaymentId— a concrete UI surface for the entry's existing 'PaymentLinkedRefund' backend claim. The refund gate is keyed off the payment's ownunappliedAmount(its unallocated balance) rather than an invoice's outstanding balance, which is a frontend-only nuance additive to, not contradicting, the entry's invoice-outstanding-cap claim (that cap is enforced server-side in PaymentsHelper.cs, untouched by this diff). Worth folding into the 'Refunds — what exists after close' section and updating the 'admin refund/void UI... not-fully-walked' note once this Refund dialog is live-walked on admin.foodops.io. Does not touch counter POS — 'no refund button on the counter POS' stands. Unrelated in the same diff: supplier/shopping-list fixes (out of this entry's scope). (auto-applied by the truth pipeline; adversarially gated)2026-07-22 — codebase (haafai/hubits-api-sales 407e704..aca3395, ReportsController.cs GetPaymentsReceived). NEW BEHAVIOR adjacent to this entry's payment-void coverage: the sales API's Payments Received report (a report not currently in this entry's Reporting table — distinct from the documented Refund History / Voids & Comps / Day Summary) now hides voided payments by default via
if (!query.IncludeVoided) payments = payments.Where(p => p.PaymentStatusId != PaymentVoid), with a newIncludeVoidedrequest flag to bring them back, and each returned row now carriesStatusName/IsVoidedso a voided payment reads clearly on screen when included. This reinforces (doesn't contradict) the entry's existing claim that PaymentVoid payments are 'excluded from sales and reconciliation calculations' (PaymentsController.cs:749-780) by extending the same exclude-by-default pattern to this report, and adds a concrete opt-in toggle worth folding into the Reporting section once the admin UI surface for this report is identified and live-walked. Unrelated in the same diff: CreditNotesController.cs's PrintPdf gained.Include(LinkedInvoices...)/.Include(LinkedCreditNotes...)to fix a null-navigation Razor throw when a credit-note PDF template renders{{Invoice Number}}on-demand — a bug fix to PDF regeneration, not a documented-behavior change, and outside this entry's cited credit-note create/allocate/unallocate surface. (auto-applied by the truth pipeline; adversarially gated)2026-07-21 — codebase (haafai/hubits-api-sales 99e6847..f26ba1f, OrdersController.cs CancelOrder). CORRECTION: the pre-close 'Cancel Order' flow documented in this entry's decision table and walkthrough is no longer open to 'any user who can see the order' — a new authorization check requires the SalesOrders.Delete role, read directly from org-scoped UserRoles (bypassing the 3-minute-cached User.IsInRole, explicitly because the API-key path doesn't populate it); callers without the role get HTTP 403 'You do not have permission to cancel orders' before any order/table/KOT state changes happen, and a warning is logged. This also updates the 'Does cancelling an order need a manager PIN?' answer — there's still no PIN dialog, but there is now a coarse role gate beyond the existing locked/closed/cancelled checks. Unrelated in the same diff: a GetCustomerBalances timezone-comparison fix (ReportsController.cs, outside this entry's cited surface) and an AllocatePayment signature change in PaymentsHelper.cs from Task to Task<(bool Success, string Message)> — the cap logic and message text ('Payment amount is greater than the invoice ({Reference}) outstanding balance.') are unchanged, so the entry's refund/allocation-cap claim stands; this is purely an internal plumbing change to surface the message to callers and isn't user-facing on its own in this diff. (auto-applied by the truth pipeline; adversarially gated)
2026-07-16 — codebase (haafai/hubits-api-sales acfa332..e0517e4, CreditNotesController.cs). NEW BEHAVIOR clarifying the entry's thin 'Credit notes' section: a credit note raised against an invoice now visibly has a two-stage lifecycle — while in Draft it carries only a pending, zero-amount link to the invoice (no balance impact), and only at Approval (via either the dedicated approve endpoint or the general status-update endpoint) does
InvoiceHelper.ActivateCreditNoteLinksfill in the real amount and recalculate both sides' totals. Separately,Allocatenow validates each target invoice's outstanding balance individually and rejects over-allocation with 'Invoice ({Reference}) only has {Outstanding} outstanding — cannot allocate {Amount} of credit to it.' (previously only the credit note's own balance was capped, so allocating credit could drive a target invoice negative — fixed as a bug).DeleteCreditNote(soft-delete) now also releases any allocation links first, since a leftover pending-link row was leaving invoices with a phantom credit from a deleted credit note. None of this contradicts the entry's existing 'create, allocate against invoices, unallocate — back-office only' claim; it adds detail worth folding into the Credit notes section once the admin UI's credit-note flow is live-walked. Unrelated in the same diff: SessionsController/OrdersController printer-registry DI swap, a cross-tenant IDOR fix on GetCustomerOutstanding, receipt-currency display fix (ADR-0011), and PaymentsController/PaymentsHelper accounting-journal and report changes (GetPaymentsReceived, deposit-account resolution, allocation-status payment filter) — none of these touch the entry's cited void/refund/cancel/session-cash-math surface (VoidPayment, PaymentType.Refund, GetRefundHistory, SessionsController expected-cash lines, DaySummaryReportService) so those claims stand unverified-but-unchallenged by this diff. (auto-applied by the truth pipeline; adversarially gated)2026-07-09 — codebase (haafai/foodops-admin-app 20beb5b..2e6faec, SalesReceiptHeaderActions.tsx). NEW BEHAVIOR: the sales receipt header actions menu (already cited in this entry's credit-notes section as showing 'credit-note tabs on sales receipts') gained a direct Issue Credit Note action — a new
onIssueCreditNoteprop wired to a dropdown item gated bycanIssueCreditNote = !isVoided && (isApproved || detail.isDraft), i.e. available on draft, approved, and partially-paid invoices, just not voided ones. Per the added comment, it opens the credit note form pre-filled from the invoice (customer, currency, addresses, line items) and links the new credit note back to the source invoice. This adds a concrete UI entry point to the entry's existing 'credit note … back-office only' claim and to the 'Related' link to Credit Notes — worth a line in the Credit notes section and possibly the decision table's account-customer-reversal row, once the admin UI is live-walked. Does not contradict any existing claim in the entry. (auto-applied by the truth pipeline; adversarially gated)2026-07-04 — codebase (haafai/hubits-api-sales 01cd718..3005390, OrdersController.cs CancelOrder). NEW BEHAVIOR: cancelling an order (the pre-close 'Cancel Order' flow documented in the walkthrough) now also reverses recipe/BOM ingredient stock that was deducted at prepare time ('legacy parity'), via a new non-fatal try/catch around order.ReverseBomStockDeduction(). The reversal outcome (order id, count of reversed line items, and any error) is logged in a new BomStockReversalLog object that's now returned in the cancel-order API response alongside Success/Message/OrderNumber. This is additional to the already-existing 'release committed stock for cancelled order' step and is worth a line in the cancellation walkthrough's step 4 (server-side effects). Note this is distinct from the entry's open 'Do voided items restock inventory?' question, which concerns line-level item voids on an open order, not full order cancellation — that question remains unconfirmed and is not resolved by this diff. (auto-applied by the truth pipeline; adversarially gated)
2026-07-04 — codebase (haafai/foodops.counter.app be4fe21..1266a30). Payment edits (
update-payment) on an open order now work offline as well as online:OrderDetailsPanel.tsx'shandleSavePaymentgained anisOfflinebranch that mirrors the edit into local offline state (editPayment) and queues it for sync (queuePaymentUpdate), showing 'Payment updated — will sync when online', mirroring the offline-cancellation pattern already documented for order cancellation. One caveat: a receipt-file attachment on a payment edit still requires connectivity — the code blocks that specific case with 'Reconnect to attach a receipt' since a File object can't sit in the offline sync queue. Worth a line in the payment-edit part of the decision table / walkthrough; does not affect refund, void, or cancellation behavior. (auto-applied by the truth pipeline; adversarially gated)2026-07-04 — codebase (haafai/foodops.counter.app be4fe21..4626f5e, OrderDetailsPanel.tsx). NEW BEHAVIOR: the counter's payment-edit flow (Edit payment from the order's payment list / EditPaymentModal, cited in the decision table and the 'payment keyed wrong' Q&A) now works offline too — handleSavePayment detects isOffline, mirrors the edit locally via a new editPayment() call, and queues it with queuePaymentUpdate() for sync once connectivity returns (toast: 'Payment updated — will sync when online'); a receipt-file attachment is the one part still blocked offline ('Reconnect to attach a receipt'). This mirrors the already-documented offline order-cancellation queueing and is worth adding alongside it. Does not contradict any existing claim — the entry never said payment edits require connectivity — so no edit to existing text, just a gap to fill. Unrelated: the diff's other hunk (loyalty-program cache warming in the same file) is out of scope for this entry. (auto-applied by the truth pipeline; adversarially gated)
2026-06-29 — codebase (July-10 launch re-verify;
foodops.counter.app@32669a9main,hubits-api-sales@19190b9main,foodops-admin-app@f6ffd0bmain). HIGH-RISK CORRECTION: the Restricted Waiter removal/decrease gate is DISABLED on the current stack —OrderUpdateService.cs:671-684detects the removal and logs it but the blockingreturn (false, "Your role does not allow…")at :682 is commented out ("Restricted Waiter removal/decrease restriction DISABLED 2026-06-17 … the save is no longer blocked"). Corrected the role-gate section, decision table, key concept, edge case, and the manager-PIN Q&A; the prior KB claim that a Restricted Waiter is blocked from removing items was FALSE for launch. LEGACY RE-POINT: the refund/payment-management/void surface moved off the retiredapp.foodops.iolegacy web app (haafai.app.foodops/Haafai.Sales/Controllers/PaymentsController.cs+_AddEdit.cshtml) to the new admin app (foodops-admin-app→ admin.salesmade.io / finance-v2.hubits.io:PaymentDetailsPage.tsx,PaymentFormDialog.tsx,VoidInvoiceDialog.tsx,SalesReceiptHeaderActions.tsx); droppedhaafai.app.foodopsfrom frontmatter. UPHELD: counterRefundManager.tsx(13-line stub) /PaymentsPage.tsx(placeholder) — no POS refund; pre-close cancel frees table + cancels KOTs + per-kitchen cancel slips +OrderCancelled/KitchenOrderAddedSignalR (OrdersController.cs:3222-3462),CancelRemarks = order.CancelRemarkon the order DTO (:2574); item-void soft-flagsIsVoided/VoidedTime/CancelledComments(OrderUpdateService.cs:581-582,693); payment void[HttpPost("{id}/void")][Authorize(Invoices.AddEdit)](PaymentsController.cs:739-743); refund paymentsPaymentType.Refund(PaymentsHelper.cs:151,283); refund-history report credit-note-based (ReportsController.cs:504-526). Residual risk: the exact legacy refund-cap message "Refund amount is greater than the payment outstanding balance." was not re-located (current code shows the invoice-outstanding cap); the admin refund/void UI is thin (no Delete/Void/Edit onPaymentDetailsPageyet) and was not live-walked — flagged for orchestrator re-check. Frontmatter SHAs + last_verified bumped.
Note: Seeded 2026-06-11 · codebase only (audit P1-10 coverage gap) Verified_by:
claude-code· Last verified: 2026-06-11 New entry authored against prod branches:foodops.counter.app@1f77fc6(main),hubits-api-sales@20fc7ca(main),haafai.app.foodops@a8ea20b(main),foodops-admin-app@bcde336(main). Key findings: counterRefundManager.tsx/SplitPaymentManager.tsx/PaymentsPage.tsxare empty stubs — no POS refund surface; pre-close cancel + item-void flows fully implemented (cancel endpoint frees tables, cancels KOTs, prints per-kitchen cancellation slips; voided lines soft-flagged with reason); Restricted Waiter server-side gate on removals/decreases; refunds are back-officePaymentType=Refundrecords linked viaPaymentLinkedRefund, capped at the original payment's unapplied balance, subtracted from session expected-cash; payment void gated toInvoices.AddEdit; API refund-history report is credit-note based. No live UI walk this pass — all "Walkthrough" labels are component-source-derived and carry UI-TODOs.
Additional verification events are appended here by the weekly KB sync and by manual re-verification passes. See _meta/failure-log.md for the global failure index.