Plain-English summary
Someone walks up to the counter, orders a shawarma and a Coke, pays cash, leaves with their food. This page walks through every button, screen, and confirmation involved in that 60-second transaction — from opening the cash drawer at the start of the shift to printing the receipt at the end. It's the most common POS flow and the one every cashier has to get right.
When you'd use this
End-to-end reference for:
- Training a new cashier on day one
- Refreshing a voice agent or chat agent's understanding of the "how do I do a quick sale?" question
- Troubleshooting when an order got stuck mid-flow (see Known Issues)
- Scripting the same flow via Playwright for regression testing
Summary
End-to-end walkthrough of creating a takeaway order on the Foodops POS: opening a register session, browsing categories, adding items, processing cash payment, and viewing the completed order receipt. Covers user stories US-POS-006 (takeaway order) and US-POS-007 (quick sale).
Prerequisites
- Restaurant has at least one outlet configured
- Menu categories and items are created and assigned to the outlet (see Known Issues)
- A register exists for the outlet
Step 1: Open a Register Session
Navigate to POS via the sidebar Counter icon (counter app routes:
/counter, then the session POS at/sessions/:id/pos; the legacy/ordersroute belonged to the retired monolith)The Sessions page shows "Open Sessions" (any active) and "Registers" (available)
- If the register already has an open session, it appears under "Open Sessions" with a "Join" button (and "Details") instead of "Open Register" — click Join to enter the existing session (steps 3–6 below apply only to a fresh register). See Session handover.
Click "Open Register" on your register

The Open Register dialog shows MVR denomination breakdown: 1000, 500, 100, 50, 20, 10, 5, 2, 1
Enter the count of each denomination in your opening cash drawer (e.g., 5× MVR 100 = MVR 500 float)
Click "Open Register"
You're now on the Counter page

Step 2: The Counter Interface
The Counter page has three sections:
Header bar:
- An "Orders" heading with a "Register N / Outlet name" subheading (there is no element labelled "Counter")
- A Search button in the order-list toolbar that opens a search input when clicked (not a persistent search bar)
- Order type buttons: Dine-in, Delivery, Takeout
Status tabs: All, Open, Preparing, Ready, Closed, Cancelled — each with a count badge (there is no "New" tab)
First-time guided tour: "You're in the counter!" (5 steps) — explains key areas. Click through or dismiss.
Step 3: Create a Takeaway Order
Click "Takeout" in the header
The order creation view opens:
Left panel — Category tabs + menu grid:
- Category tabs displayed horizontally across the top of the menu panel (e.g., Beverages, Desserts, Shawarmas, Sides)
- Click a category to show its items as cards with name and price (inc. tax)
- Search by name or SKU at the top

Right panel — Order summary:
- Order Type: Takeout (one word in the order-creation panel; the Order Details panel shows it lowercase as "takeout")
- Customer: Walk In Customer (editable)
- Pickup Time (editable)
- Notes are added via the Add Notes button below (opens a dialog with Customer Notes + Internal Notes; customer notes appear on the receipt) — not a persistent field
- Item list with quantity controls (-, qty, +) and delete button
- Totals block: Items Total, Sub Total, GST (8% shown as a sub-label), Total
- Action buttons: Discount, Add Notes, Save & Accept, Go to Payment, Save as draft
Browse and add items:
- Click a category (e.g., "Shawarmas") to see items
- Click an item card to add it to the order (qty 1)
- Click again to increment, or use the +/- controls
- Switch categories to add items from different categories

Example: Chicken Shawarma (MVR 45.00) + Coca-Cola (MVR 15.00) = Subtotal 60.00, Tax 4.80, Total MVR 64.80
Step 4: Process Payment
Click "Go to Payment" — the order is automatically saved and accepted
The order detail and payment panel opens on the right side of the POS page (the URL stays
/sessions/:id/pos— it is not a separate page):
Order Details: Order ID (SO-NNNN/YYYY), status (Accepted), items, totals Customer Details: Name, Contact No. Settle Payment: Amount due, payment method selection
Select payment method: Click Cash, Transfer, or Card
- Cash: pre-fills the full amount, editable for exact change
- Transfer: bank transfer
- Card: card terminal payment

Click "Pay & Close" to process payment and close the order

Payment Completed view shows:
- Pending: MVR 0.00 (fully paid)
- Cash Balance: MVR 0.00
- Payment method: Cash, MVR 64.80
- a print icon button (unlabeled — positioned before the Tax Invoice button)
- Tax Invoice button
- Email and share (icon) options
Order Lifecycle
| Status | Meaning |
|---|---|
| New | Order just created, not yet accepted |
| Open | Accepted, waiting for preparation |
| Preparing | Kitchen has started working on it |
| Ready | Food is ready for customer pickup |
| Closed | Payment processed, order complete |
| Cancelled | Order was cancelled |
For takeaway, the typical flow is: New → Accepted (auto) → Closed (on payment).
Offline behavior (NEW 2026-05)
The POSPage is offline-first: orders can be entered and submitted even when the device loses internet connectivity. When offline, each order is assigned a tempId — a locally generated identifier — instead of waiting for the server to issue a real order ID.
Once connectivity is restored, the POS syncs the pending order to the server. The server issues a permanent serverId (e.g., SO-0042/2026) to replace the tempId. The OrderCard component preserves its identity across this swap — the card does not flicker or re-render disruptively during the tempId → serverId transition (commits 244ef17, a4f0488). From the cashier's perspective, the order card stays in place and simply updates its reference number when the sync completes.
The POSPage also uses stale-while-revalidate caching for session details and order action buttons, so the UI remains usable and responsive during brief connectivity drops (commit 68e0bf7).
Printing is decoupled from saving. Saving/accepting an order does NOT block on the local print broker — KOT and receipt prints are handed to the broker (or queued to brokerQueue and drained by a background worker when the broker is slow/unreachable), so a stalled or offline printer never holds up the order. Replays are deduped by a stable requestId carried on the queued payload — derived from the target, e.g. receipt:<serverId> for a receipt and kot:<serverId>:<kotId> for a KOT. It is used twice: locally, to skip enqueuing when an unfinished print for the same target is already queued, and server-side, where the same requestId makes a replayed job idempotent. [code: foodops.counter.app/src/offline/actions/printActions.ts queueKotPrint/queueReceiptPrint — direct fire, fall through to queue on failure @ main]
Offline indicator — 2026-05-04 finding SUPERSEDED at launch
Warning: The "no offline indicator" finding below was observed on the retired legacy
app.foodops.io/orders/indexserver-rendered shell — that surface is gone at the July-10 launch. The new counter PWA ships a visible offline/sync indicator:src/components/offline/OfflineIndicator.tsxplusSyncStatusBadge.tsx/SyncStatusListener.tsxare mounted viasrc/main.tsx[code: foodops.counter.app/src/components/offline/OfflineIndicator.tsx; src/main.tsx @ 32669a9]. The screenshots and analysis below are kept only as a record of the legacy observation; do NOT cite them as current behaviour.

Finding (LEGACY app.foodops, superseded): no visible offline indicator UI in that build. Playwright offline mode was induced via context.setOffline(true) plus navigator.onLine override and window.dispatchEvent(new Event('offline')). The network layer immediately detected the drop (SignalR WebSocket connections failed with ERR_INTERNET_DISCONNECTED; /orders/getkotstatus, /restaurantOrders, and /printhub endpoints all returned disconnection errors). Despite confirmed network loss, the /orders/index POS view showed no banner, toast, icon, or badge indicating offline status.

Clicking "Save & Accept" while offline (on an empty cart) triggered no user-visible feedback — neither an error toast nor an offline-queuing message appeared. The console recorded 36+ network errors but the UI remained silent.

On restoring the connection (context.setOffline(false) + online event dispatch), the UI also showed no "back online" or "syncing" indicator — the page returned to the same appearance as before.

Interpretation: The OfflineIndicator component referenced in the foodops.counter.app codebase may not yet be rendered in the production build at app.foodops.io/orders/index, or it may only surface after an order has been saved with a tempId (which requires a server-connected first save to obtain the session context). The offline-first data flow (tempId → serverId swap, stale-while-revalidate caching) is confirmed by code commits, but the visual indicator for cashiers has not been observed in E2E testing as of this date.
Action for next pass: Attempt offline injection after saving at least one live order (requires a working item-add flow). The sidebar has a hidden #sidebarFailedOrdersAlert element (count badge) which may appear if a failed order is queued.
2026-05-04 — captured via playwright
context.setOffline(true)+ offline event dispatch. Offline behavior confirmed at network layer; no UI indicator observed. Real-world cellular handover may surface the indicator in the React sub-app context if it differs from the server-rendered shell.
Receipt Options
After payment, 4 options:
- Print Receipt — sends to configured receipt printer
- Tax Invoice — formal tax document
- Email — send digital receipt to customer
- Share — share via link
Known Issues
Warning: Both known issues below are legacy
haafai.app.foodops(Blazor monolith) artifacts — jQuery modal handlers and the/menucategories/edit/{id}Razor full-page edit. The legacy monolith is retired at the July-10 launch, so these specific failure modes do not apply to the new stack (counter PWA +foodops-admin-appback-office). Outlet/category assignment now happens in the admin app; "Reload Cache" still exists on the counter. Retained only as historical context.
Items Not Showing in POS After Creation (LEGACY monolith)
- Symptom: Categories show on POS but say "No categories available" or items don't appear
- Cause: Items must be explicitly assigned to an outlet via the "Items sold at Outlets" checkbox in the item edit form. During batch item creation, this checkbox may not persist correctly if set via the create dialog — need to edit each item afterward and check the outlet.
- Root cause: The "Items sold at Outlets" checkbox in the Add Item modal uses jQuery events. If the checkbox click is intercepted or doesn't trigger the correct handler, the outlet assignment silently fails.
- Workaround: After creating items, go to each item's detail page → Edit → verify "KB Test Outlet" checkbox is checked under "Items sold at Outlets" → Save. Then reload POS cache.
- Also: After assigning categories to the Menu Group, click "Reload Cache" on the POS counter page to refresh the local menu data.
Menu Group Category Assignment Bug (LEGACY monolith)
- Symptom: Checking category checkboxes in the Menu Group edit modal and clicking Save doesn't persist
- Cause: The modal dialog form submission may silently fail (jQuery dependency issue)
- Workaround: Submit the form data directly via the full-page edit URL (
/menucategories/edit/{id}) using a direct POST request with the CategoryIds
Key concepts
- Register session — the open/close cycle of a cash drawer. Opens with a float amount (opening cash), closes with reconciliation. Every order during the session is tied to it.
- Float — the opening cash the cashier counts into the drawer at start of shift. Entered in denominations so the count is traceable.
- Order type — Dine-in / Takeout / Delivery. Each has a different order form (Takeout defaults to Walk-In, Dine-in requires a table, Delivery needs an address).
- Reload Cache — the POS caches menu data locally for speed; settings changes only appear after a manual cache refresh.
- Order lifecycle — New → Open → Preparing → Ready → Closed (or Cancelled). Takeaway usually skips Open/Preparing if paid immediately.
- Tax Invoice vs Receipt — Receipt is the everyday slip; Tax Invoice is the formal GST document with customer TIN. Tax Invoice requires a registered customer attached to the order.
- Service-charge GST on the receipt — when an order carries a service charge, its GST is reconciled into the receipt's single GST line (the service charge is shown as its pre-tax base), so the printed receipt reconciles to the order Total. Manual reprints also keep the correct org-local order time and retain the customer note.
[code: hubits-api-sales/.../OrderPrintController.cs @ 66fdd0a6 + OrderTaxBreakdown.cs @ 6be4eba9; foodops.counter.app parseServerDate.ts / resolveServerOrderNotes.ts @ main] - Pay & Close — single click that records payment AND transitions the order to Closed. Different from Save & Accept (just marks ready for kitchen).
- tempId — a locally generated order identifier assigned when the POS is offline. Replaced by a permanent server-issued ID (serverId) once connectivity is restored and the order syncs. The OrderCard preserves its visual identity across this swap.
Common questions
Q: How do I handle a customer paying with cash + card combined? A: On the payment screen, enter the cash amount, select Cash, tap Apply; then enter the card amount, select Card, tap Apply. Both settlements attach to the order. Pay & Close only fires when the total settled equals the order total.
Q: Can I add a discount after the order is already accepted? A: Yes — reopen the order from the status tabs, click Discount, enter reason + amount. Audit trail captures the user, time, and reason.
Q: What if the customer changes their mind and cancels mid-order? A: Use Save as draft to hold without firing to kitchen. Or Cancel to void. Drafts don't deduct stock; cancellations of accepted orders require a reason.
Q: How do I take an order for a walk-in group that'll pay at the end? A: That's a dine-in workflow, not takeaway — see Dine-In & Tables. Takeout is pay-at-counter; Dine-in supports running tabs.
Q: The Tax Invoice button doesn't print customer details — why? A: Customer was Walk-In (anonymous). Attach a real customer record first (see Customers & CRM), then regenerate the invoice.
Q: What happens to the cash drawer if I log out mid-session? A: The session stays open — another user with the right role can join the same session and continue. The cash drawer state is tied to the session, not the logged-in user.
Q: How do I close out at end of day?
A: From /orders → the open session → Details → Close. You'll enter counted cash denominations; the system computes the expected vs actual delta for reconciliation.
Q: What happens if I lose internet mid-order? A: The POS continues working offline. Your in-progress order gets a temporary local ID (tempId). When connectivity returns, the POS syncs automatically — the order is submitted to the server, which assigns a permanent order number. The order card stays visible and stable throughout; there is no data loss. Payment cannot be finalized until the sync completes and a real order ID is confirmed.
Test Order Created
| Field | Value |
|---|---|
| Order ID | SO-0001/2026 |
| Type | Takeout |
| Customer | Walk In Customer |
| Items | 1× Chicken Shawarma (MVR 45.00), 1× Coca-Cola (MVR 15.00) |
| Sub Total | MVR 60.00 |
| GST (8%) | MVR 4.80 |
| Total | MVR 64.80 |
| Payment | Cash MVR 64.80 |
| Status | Closed |
Related
Menu Management — creating categories and items
Onboarding — initial outlet and register setup
Sales Sessions & Register Management — session lifecycle and cross-device sync
User Stories: POS — US-POS-006, US-POS-007, US-POS-014, US-POS-015
2026-08-23 — codebase (hubits-api-sales df4179d..0f25183, main; SessionsController.cs, OrderPaymentController.cs, OrdersController.cs, OrderUpdateService.cs — within this entry's tracked Controllers/** and Services/Counter/Orders/** surface). Most of this diff reproduces findings already logged in prior entries: the InvoiceSettlement.IsSettled refinement to UpdatePayment (2026-08-18), the GeneratePaymentDocumentContent eager-load + GeneratedContentPersistence fix (2026-08-16), the GetSessionDetails TagWith(RecompileHintInterceptor.Tag) plan-cache opt-out (2026-08-16), the OpenedTime UTC-ISO fix and OpenRegister ExpectedAmount-seeds-to-float (2026-08-08), and auto-close-on-settlement via TryCloseIfTerminalAndSettledAsync (2026-08-08) — no new behavior in any of those. Three hunks ARE new, all backend-only with no frontend/UI evidence in this diff. (1) NEW ENDPOINT: OrdersController gains POST {orderId}/change-discount (ChangeOrderDiscount), which adds a discount to an order that currently has none — it explicitly refuses (200, Applied=false, with a Reason) rather than errors when the order already carries a discount, has a loyalty redemption, is closed/cancelled, or has any payment tendered against its invoices. Per the code's own summary, it deliberately never replaces or removes an existing discount because nothing persisted distinguishes an auto-applied discount from a cashier-keyed one. This doesn't contradict this entry's Common Questions line ('Can I add a discount after the order is already accepted? Yes — reopen the order... click Discount, enter reason + amount') since no UI evidence here confirms whether this endpoint is that flow's backend or a separate one — but the discount-already-present/has-payment refusal semantics are previously-undocumented nuance worth a line if this entry's Discount Q&A is ever expanded. (2) Supporting fix: OrderUpdateService.UpdateOrder's item-level-loyalty-fold logic is made null-safe for LineItems (updateOrderDto.LineItems?...), since the new header-only ChangeOrderDiscount call (like the pre-existing ChangeOrderLoyalty) passes LineItems=null and previously NRE'd through this code path — internal-only, no cashier-visible change. (3) SessionsController.GetMySessionsAndRegisters now also returns EnforceUniqueBlazeNumber (previously session-DETAILS-only per the 2026-08-11 entry) at the payment/session-list surface, plus a new EnableQuickSessionSwitch field sourced from the outlet — both backend DTO additions with no UI evidence in this diff of a consuming warning or quick-switch control. Neither contradicts any documented claim; the EnforceUniqueBlazeNumber extension is the same still-open watch-item flagged 2026-08-11 ('worth a line in Step 4 or Common Questions... once UI evidence of the warning surfaces'), now reaching one more surface but still not confirmed cashier-facing. No edit needed to existing prose. (auto-applied by the truth pipeline; adversarially gated)
2026-08-18 — codebase (hubits-api-sales df4179d..98c202d, main; SessionsController.cs, OrderPaymentController.cs, OrdersController.cs, OrderUpdateService.cs, PaymentProcessingService.cs, TenderMath.cs — within this entry's tracked Controllers/** and Services/Counter/Orders|Payment/** surface). This range extends past the already-verified 8808c93 point (2026-08-16 entry). Everything up to that point reproduces findings already logged: the EnforceUniqueBlazeNumber session-DTO field, the OpenedTime UTC-ISO fix, OpenRegister's ExpectedAmount-seeds-to-float, the TagWith(RecompileHintInterceptor.Tag) plan-cache opt-outs on GetSessionDetails/GetOrdersList, the GeneratePaymentDocumentContent eager-load + GeneratedContentPersistence fix, the auto-close-on-settlement path (Pay/MakeCreditPayment/ChangeStatus via TryCloseIfTerminalAndSettledAsync), CancelOrder's wasRejectedBeforeAccept reject-notification, OrderUpdateService's BOM-reversal-on-edit, the post-commit non-fatal stock-cache invalidation refactor, TransferReferenceValidator's pre-write Bank Transfer reference enforcement, and TenderMath.Book's foreign-tender booking fix — no new behavior in any of those. Two hunks beyond 8808c93 ARE new, both backend-only with no UI evidence and no cashier-visible label/button touched. (1) OrderPaymentController.UpdatePayment's invoice-status-on-payment check changes from a raw
invoice.Outstanding <= 0comparison toInvoiceSettlement.IsSettled(invoice.Outstanding)— an internal refinement (likely a rounding/tolerance-aware settlement check) to when an invoice flips to FullyPaid; this entry doesn't document invoice-status internals, so nothing is contradicted. (2) PaymentProcessingService.MakePayment's deposit-account resolution (payment-method→ChartOfAccount map + undeposited-account fallback, previously fetched mid-transaction as 'Step 3') is hoisted to a pre-strategy fetch outside the transactional execution-strategy block — a 2026-08-09 lock-contention fix so this reference-data read no longer runs while the settle holds locks on ProductVariants/Invoices (the helper now throws if called inside a transaction). This doesn't change any documented Step 4 payment behavior — Cash/Transfer/Card selection, Pay & Close, and journal posting all produce the same result, just with the account-map lookup timed differently. No edit needed to existing prose. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-08-16 — codebase (hubits-api-sales df4179d..8808c93, main; SessionsController.cs, OrderPaymentController.cs, OrdersController.cs, OrderUpdateService.cs, PaymentProcessingService.cs, TenderMath.cs — within this entry's tracked Controllers/** and Services/Counter/Orders|Payment/** surface). Nearly everything in this diff reproduces findings already logged in the 2026-08-03 through 2026-08-12 verification entries: auto-close-on-settlement (Pay/MakeCreditPayment/ChangeStatus via TryCloseIfTerminalAndSettledAsync), OpenedTime UTC-ISO fix, OpenRegister ExpectedAmount-seeds-to-float, the EnforceUniqueBlazeNumber session-DTO field (settings fetch only), CancelOrder wasRejectedBeforeAccept reject-notification, OrderUpdateService's BOM-reversal-on-edit, the post-commit non-fatal stock-cache invalidation refactor, and TenderMath.Book's foreign-tender booking fix — no new behavior in any of those. Three hunks ARE new. (1) Backend-only, no UI evidence: GetSessionDetails (SessionsController.cs) and GetOrdersList (OrdersController.cs) both gain TagWith(RecompileHintInterceptor.Tag) query hints, a plan-cache opt-out addressing 2026-08-09 timeout storms — no behavioral or cashier-visible change. (2) Backend robustness fix: OrderPaymentController.GeneratePaymentDocumentContent now eager-loads Transaction.Customer, Transaction.TransactionCurrency, and Invoice.Transaction.TransactionCurrency, and persists via a new single-column GeneratedContentPersistence helper instead of a tracked-graph SaveChangesAsync — fixes a RuntimeBinderException that could previously kill background payment-document (print template) generation when those navigations were unloaded. This entry's Receipt Options section doesn't currently describe this failure mode, so nothing is contradicted; it's a reliability fix for receipt/tax-invoice content generation, not a new feature. (3) NEW ENFORCEMENT, most relevant to this entry: PaymentProcessingService.MakePayment now calls TransferReferenceValidator.ValidateBatchAsync on all tendered payment methods BEFORE any DB writes, rejecting the checkout outright (IsSuccess:false with a returned error message) if a Bank Transfer reference is missing (when 'Soft Enforce Payment Reference Number' is on) or a duplicate within the session (when 'Enforce Unique Blaze Number' is on) — Bank Transfer tenders only, others pass through untouched. This is the first confirmed enforcement behind the EnforceUniqueBlazeNumber field flagged as backend-only groundwork in the 2026-08-11 entry ('gates whether a Transfer Receipt whose reference was already used this session raises a warning... Worth a line in Step 4 or Common Questions... once UI evidence of the warning surfaces') — it has now escalated from a settings fetch to an actual checkout-blocking validation. This entry's Step 4 currently documents Transfer only as 'Transfer: bank transfer' with no mention of a reference field or validation, and the Common Questions section doesn't address it either, so nothing existing is contradicted — but this is a previously-undocumented, cashier-facing failure mode for the Transfer tender worth adding once UI evidence of how the error is surfaced (toast, inline field error, etc.) is confirmed. No edit needed to existing prose. (auto-applied by the truth pipeline; adversarially gated)
2026-08-12 — codebase (hubits-api-sales df4179d..ecfea28, main; SessionsController.cs, OrderPaymentController.cs, OrdersController.cs, OrderUpdateService.cs, PaymentProcessingService.cs, TenderMath.cs — within this entry's tracked Controllers/** and Services/Counter/Orders|Payment/** surface). This range extends past the already-verified 7273baf point (2026-08-11 entry). Nearly everything in the diff reproduces findings already logged in the 2026-08-03 through 2026-08-11 verification entries: the EnforceUniqueBlazeNumber session-DTO field, the OpenedTime UTC-ISO fix, the OpenRegister ExpectedAmount-seeds-to-float change, the OrderPaymentController.cs/OrdersController.cs auto-close-on-settlement (Pay, MakeCreditPayment, ChangeStatus via TryCloseIfTerminalAndSettledAsync), the CancelOrder wasRejectedBeforeAccept reject-notification, OrderUpdateService.cs's BOM-reversal-on-edit, PaymentProcessingService.CheckAndCreateShoppingList's orgId param, and TenderMath.Book's foreign-tender booking fix — no new behavior in any of those. Two hunks ARE new, both backend-only with no frontend evidence and no cashier-visible label/button touched. (1) SessionsController.GetSessionDetails now includes Transaction.Customer and adds a CustomerName field (defaulting "Walk-in Customer") to each invoice DTO — session-details/close-out invoice listing, not this entry's documented Takeout walkthrough or its 'How do I close out at end of day?' Q&A, which doesn't currently describe an invoice list at all. (2) PaymentProcessingService.cs's stock-cache invalidation is refactored from two pre-commit calls (inside CreateInvoiceFromOrder and ApproveInvoiceInternal, holding up the DB transaction/lock on Redis+SignalR latency and risking a reader repopulating from pre-commit state) to a single post-commit, non-fatal (try/catch-logged) call per caller — a correctness/latency fix with no behavioral or UI-visible change. Neither hunk contradicts any documented claim in this entry (Steps 1-4, Order Lifecycle, Offline behavior, Receipt Options, Key Concepts) or any tracked sidecar claim (print-broker decoupling, requestId dedup, offline indicator, service-charge GST reconciliation, reprint org-local time/customer-note retention) — none of those files are touched by this diff. No edit needed to existing prose; the CustomerName addition would be worth a line in sales-sessions.md if that entry ever documents the session-details invoice list. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-08-12 — codebase (foodops.counter.app a00083c..ac27bea, main; FailedSyncSection.tsx, SyncStatusListener.tsx, SyncService.ts, plus syncPolicy.ts extraction and new/updated tests in syncDependencyGiveup.test.ts, syncDuplicateCustomerRecovery.test.ts, syncPolicy.test.ts, syncService.lifecycle.test.ts, poisonedCustomerRecovery.test.ts — within this entry's tracked src/offline/** and src/components/offline/** surface). This diff range fully reproduces findings already logged in the 2026-08-03 and 2026-08-04 verification entries — no behavior beyond what's already captured. The 'order was synced as Walk In' toast (SyncStatusListener's new order-customer-detached listener), the FailedSyncSection retry-toast copy change ('Item is no longer retriable' → 'Retry cannot fix this one — see the error for what to do'), the healUnresolvableLocalCustomers 20s startup self-heal sweep, the isDrainVisible()/SESSION_AGNOSTIC_ENTITY_TYPES extraction to syncPolicy.ts (now covering customer/customerUpdate/deliveryAddress as org-scoped, session-agnostic types), the existingCustomerId fast-path for duplicate-customer recovery, and the hardened phone-matching fallback (legacy 'phone' field, country-code tolerance, refuse-on-ambiguous-match) were all already described in the 2026-08-03 entry ('poisoned customer' defenses + FailedSyncSection copy) and the 2026-08-04 entry (isDrainVisible extraction + duplicate-customer fast path). Nothing in this diff touches order creation, payment, the tempId→serverId swap, printing, or any cashier-visible label/button this entry's Steps 1–4 document, and nothing contradicts any currently-written claim (including the tracked sidecar claims about the offline/sync indicator mounting or print-broker decoupling, none of which this diff's hunks touch). No edit needed to existing prose. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-08-11 — codebase (hubits-api-sales df4179d..7273baf, main; SessionsController.cs GetMySessionsAndRegisters — within this entry's tracked Controllers/SessionsController.cs surface). Most of this diff reproduces findings already logged in the 2026-08-03 through 2026-08-09 verification entries: the OrderPaymentController.cs/OrdersController.cs auto-close-on-settlement (Pay, MakeCreditPayment, ChangeStatus), the CancelOrder wasRejectedBeforeAccept reject-notification, the SessionsController OpenedTime UTC-ISO fix, the OpenRegister ExpectedAmount-seeds-to-float change, the new RecomputeExpectedAmountsAsync close-path helper, OrderUpdateService.cs's BOM-reversal-on-edit, PaymentProcessingService.CheckAndCreateShoppingList's orgId param, and TenderMath.Book's foreign-tender booking fix — no new behavior in any of those. One hunk IS new: GetMySessionsAndRegisters now fetches the org-level 'Enforce Unique Blaze Number' setting (ApplicationOrganisationSettings key EnforceUniqueBlazeNumber) and returns it as a new SessionDto.EnforceUniqueBlazeNumber field. Per the code's own comment, this gates 'whether a Transfer Receipt whose reference was already used this session raises a warning' — directly adjacent to this entry's documented Transfer payment method (Step 4: 'Transfer: bank transfer'), which currently says nothing about a reference field or a duplicate-reference warning. This diff excerpt is backend-only (a settings fetch plus a DTO field) with no frontend evidence of a reference-entry field or warning UI consuming it, so it reads as backend groundwork rather than a confirmed cashier-facing capability yet — similar in shape to the still-open 2026-07-10 per-tender-currency watch-item. No edit needed to existing prose since no currently-written claim is contradicted; worth a line in Step 4 or Common Questions ('does the POS warn me if I reuse a transfer reference?') once UI evidence of the warning surfaces. (auto-applied by the truth pipeline; adversarially gated)
2026-08-09 — codebase (hubits-api-sales df4179d..7594413, main; SessionsController.cs, OrderPaymentController.cs, OrdersController.cs, OrderUpdateService.cs, PaymentProcessingService.cs, TenderMath.cs — within this entry's tracked Controllers/** and Services/Counter/Orders|Payment/** surface). Most of this diff reproduces findings already logged: the OrderPaymentController.cs/OrdersController.cs auto-close-on-settlement (Pay, MakeCreditPayment, ChangeStatus) and CancelOrder reject-notification hunks match the 2026-08-08 entry; the SessionsController.cs OpenedTime UTC-ISO fix and OpenRegister ExpectedAmount-seeds-to-float hunks also match 2026-08-08; the CloseRegister void-bin-review gate matches 2026-08-03; OrderUpdateService.cs's BOM-reversal-on-edit matches 2026-08-06; PaymentProcessingService.CheckAndCreateShoppingList's added orgId param matches 2026-08-06. No new behavior in any of those. Two hunks ARE new. (1) SessionsController gains a private RecomputeExpectedAmountsAsync, called from the close paths to recompute each balance's ExpectedAmount directly from payments+withdrawals rather than relying on the debounced Hangfire-driven UpdateTotals recompute — a correctness fix so a payment settled seconds before Close is reflected in Expected. This entry's 'How do I close out at end of day?' Q&A already says the system computes an expected-vs-actual delta at close; this fix makes that computation more reliable but doesn't change the documented behavior or add a new step. (2) PaymentProcessingService.cs's per-tender booking logic is extracted into a new pure TenderMath.Book() (with a TenderBooking record). For a foreign-currency tender, Payment.Amount was previously set to the tender's NATIVE applied amount while BaseAmount held the Base figure — a bug the new code comment explains under-relieves receivables and can drive UnappliedAmount negative; Book() now books Amount as the Base figure for both foreign and base tenders. For a Base-currency tender (the only kind this entry's Cash/Transfer/Card walkthrough documents, since native==base when rate=1) the resulting values are unchanged, so nothing in Step 4's payment flow is affected. No currency-selector UI evidence appears in this diff, consistent with the still-open 2026-07-10/2026-07-23 watch-items that per-tender multi-currency remains backend groundwork. Neither new hunk touches any cashier-visible label, button, or documented claim in this entry — no edit needed to human-curated sections; RecomputeExpectedAmountsAsync would be worth a line in sales-sessions.md's close-out reconciliation section if that entry covers the mechanics, and the TenderMath.Book fix is only actionable here once a foreign-tender UI surfaces. (auto-applied by the truth pipeline; adversarially gated)
2026-08-08 — codebase (hubits-api-sales df4179d..cdb3b32, main; OrderPaymentController.cs MakePayment/MakeCreditPayment, OrdersController.cs ChangeStatus, SessionsController.cs — within this entry's tracked Controllers/OrderPaymentController.cs, Controllers/OrdersController.cs, Controllers/SessionsController.cs surface). NEW BACKEND BEHAVIOR, not contradicting any documented claim: orders now auto-close on settlement. If an order has already reached its terminal SERVICE status for its type (dine-in Served / takeout Picked / delivery Delivered), a plain Pay, a Credit sale, or a ChangeStatus transition (via new TryCloseIfTerminalAndSettledAsync, mirroring OrderPaymentController.IsInTerminalServiceStatus) now closes the order automatically — whichever of 'settled' and 'terminal service status' happens last triggers the close. MakePaymentResponse.OrderClosed, previously hardcoded false on the Pay path, now reflects this; the success message becomes 'Payment processed and order closed successfully' / 'Credit sale recorded and order closed successfully'; SignalR now also fires OrderClosed plus a NotifyOrderClosedAsync webhook. This entry's Key Concepts describes 'Pay & Close' as the single click that both pays and closes, and the Order Lifecycle table doesn't model a 'Picked'/'Served' state distinct from Ready — for the documented takeaway flow (Go to Payment → Pay & Close, before the order is ever marked picked up), this auto-close path isn't the one exercised, so nothing currently written is contradicted. It would become relevant if this entry ever documents a 'pay while still Ready, mark Picked later' path or a plain 'Pay' (not 'Pay & Close') action, neither of which the walkthrough currently describes. Also in-range and out of this entry's cashier-facing scope: SessionsController.cs OpenedTime is now emitted as an explicit UTC ISO-8601 string (previously zone-less, causing a browser-local misread of a just-opened register's time) and a just-opened register's ExpectedAmount balance now seeds to the opening float instead of 0 (previously showed Expected: 0.00 until the first payment settled) — both belong, if documented anywhere, in sales-sessions.md, not here. The OrderUpdateService.cs BOM-reversal-on-edit hunk and the void-bin CloseRegister gate in this same diff range reproduce findings already logged in the 2026-08-06 and 2026-08-03 verification entries — no new behavior there. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-08-06 — codebase (hubits-api-sales df4179d..7c2d16c, main; OrderUpdateService.cs, PaymentProcessingService.cs — within this entry's tracked Services/Counter/Orders/** and Services/Counter/Payment/** surface). The SessionsController.cs OpenedTime-format fix, the CloseRegister void-bin-review gate, and the OrdersController.cs customer-notification additions (wasRejectedBeforeAccept reject-notify, ChangeStatus NotifyOrderStatusChangeAsync) reappear in this wider diff range but are NOT new — identical hunks already logged in the 2026-08-03 and 2026-08-05 verification entries. What IS new in this range: NEW BACKEND BEHAVIOR, not contradicting any documented claim — editing an already-accepted order (OrderUpdateService.UpdateOrder) now reverses BOM-linked ingredient stock for prepared line items that get voided outright (full quantity restored) or have their quantity reduced (only the delta restored), via a new ReverseBomStockDeductionForQuantity call, wrapped non-fatally per line (logged, not thrown) so a stock hiccup can't fail the edit. This mirrors, for order EDITS, the reversal this entry's Key Concepts already notes for order CANCELLATION (2026-07-XX finding) and complements the Pay & Close BOM deduction — but this entry's walkthrough doesn't currently describe editing an already-accepted order's line items at all, so nothing existing is confirmed or contradicted. Worth a line in Key Concepts (BOM deduction ↔ reversal, now covering edit as well as cancel) if post-acceptance order editing is ever documented here. Separately, PaymentProcessingService.CheckAndCreateShoppingList gained an orgId parameter at both call sites purely so the Hangfire background job (no ambient tenant context) scopes its low-stock dedupe/insert correctly — internal-only, no cashier-visible change. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-08-05 — codebase (hubits-api-sales df4179d..9351716; SessionsController.cs, OrdersController.cs — within this entry's tracked surface). The CloseRegister void-bin-review gate ('Review and clear the void bin before closing the register.') reappears in this diff but is NOT new — it's the same hunk already logged in the 2026-08-03 verification entry (df4179d..0c16730 is an ancestor of this range); no new close-out behavior beyond what's already captured. Also in-range and out of this entry's cashier-facing scope: GetMySessionsAndRegisters now emits OpenedTime as an explicit UTC ISO-8601 string (DateTime.SpecifyKind(...,Utc).ToString("O")) instead of a zone-less "yyyy-MM-dd HH:mm:ss" — a timezone-display bug fix (per the diff's own comment, a naive string was being read as browser-local and showing raw UTC, e.g. 03:02 instead of 08:02 in UTC+5) affecting session-list rendering, not anything this entry documents. NEW USER-FACING (customer-side, not cashier-side) CAPABILITY: OrdersController gains an injected ICustomerOrderingApiNotificationService. (1) In CancelOrder, a new wasRejectedBeforeAccept check (true only when the order's RestaurantOrderOperationStatusId was still New, i.e. never accepted) fires a customer notification tagged Rejected — the code comment is explicit that 'cancelling an order the counter never accepted IS the Reject button — there is no separate reject endpoint.' A cancel of an already-accepted order deliberately stays silent (no customer text), per the comment, to avoid notifying on every POS/dine-in void. (2) ChangeStatus now also calls NotifyOrderStatusChangeAsync on every status transition, so the customer ordering app/SMS reflects Preparing/Ready/etc. Both calls are non-fatal (try/catch + log only). This doesn't contradict this entry's Order Lifecycle table or its Common Questions line that 'cancellations of accepted orders require a reason' — it adds a previously-undocumented customer-notification side effect to cancel/reject and status changes that this entry (written from the cashier's POV) doesn't currently mention. Worth a line in Key Concepts or Common Questions ('does the customer get notified when I reject/cancel or update an order's status?') if this entry or a customer-ordering-app-facing doc is ever expanded; no edit needed to existing prose since nothing currently written is contradicted. (auto-applied by the truth pipeline; adversarially gated)
2026-08-04 — codebase (foodops.counter.app a00083c..5167f45, main; SyncService.ts isDrainVisible/SESSION_AGNOSTIC_ENTITY_TYPES extracted to syncPolicy.ts, plus syncDependencyGiveup.test.ts, syncDuplicateCustomerRecovery.test.ts, syncPolicy.test.ts — within this entry's tracked src/offline/** surface). Beyond the poisoned-customer self-heal already logged in the 2026-08-03 entry (SyncStatusListener toast, healUnresolvableLocalCustomers, FailedSyncSection copy — all unchanged in this range), two further internal sync-robustness fixes land, neither contradicting any documented claim. (1) The session-agnostic entity-type allow-list (previously a local Set inside sweepOrphanedPendingEntities: orderCancel, session-leave, orderLock, print) is extracted to a shared isDrainVisible()/SESSION_AGNOSTIC_ENTITY_TYPES helper in syncPolicy.ts and now also includes customer, customerUpdate, and deliveryAddress — these are org-scoped master data, not session-scoped, so tagging them with a sessionId previously made a customer-create row invisible to the drain after a register switch, in turn making it an unsyncable parent for every order queued behind it. A companion fix makes the dependency-wait check (hasUnresolvedDependencies) use the same isDrainVisible() predicate the drain itself uses, so a parent stuck in another, abandoned session is now recognized immediately instead of being wrongly treated as 'in flight' and making its child wait out the full 5-minute MAX_DEPENDENCY_WAIT_MS before dying as 'wedged'. (2) Duplicate-customer recovery (the fallback when an offline-queued /customers create 400s because the customer already exists server-side) gains a deterministic fast path: if the 400 body now carries an existingCustomerId (top-level or wrapped in {data:...}), that id is used directly with no search — replacing the phone-search guesswork blamed for the Shawarmashi 2026-07-24 incident — while still ignoring a temp id if one is echoed back, since a temp id is never a real server identity. The phone-search fallback (used when no existingCustomerId is present) is also hardened: it now matches a locally cached customer via a legacy 'phone' field (not just 'mobile'), tolerates country-code/formatting differences via new phonesMatch/phoneSearchVariants helpers, and explicitly refuses to link — fails rather than guesses — when a query matches two different customers. All of this is offline-sync internal correctness work around customer creation/dedup and session-scoped queue draining; it doesn't touch order creation, payment, the tempId→serverId order swap, printing, or any cashier-visible label/button this entry documents. No edit needed to existing prose; worth a line only if this entry is ever expanded to cover offline customer-creation edge cases specifically, which it currently isn't. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-08-03 — codebase (hubits-api-sales df4179d..0c16730, main; SessionsController.cs CloseRegister — within this entry's tracked SessionsController.cs surface). NEW BACKEND VALIDATION, not contradicting any documented claim: CloseRegister now blocks the close if the session has any VoidBinEntries with ReviewStatusId===Pending (scoped to the session + org), returning Success:false with the message 'Review and clear the void bin before closing the register.' This entry's 'How do I close out at end of day?' Q&A currently only documents entering counted cash denominations and the system computing the expected-vs-actual delta for reconciliation — it says nothing about voids or a void bin, so nothing existing is contradicted, but the close flow now carries an additional precondition this entry doesn't describe. No frontend evidence in this diff excerpt of a void-bin review UI (a list, modal, or approve/reject controls) — reads as backend-only gating so far. Worth a line in the close-out Q&A or a new Key Concepts entry ('void bin' — an entirely new concept to this entry) once/if a corresponding UI is confirmed; no edit needed to existing prose since no currently-written claim is contradicted. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-08-03 — codebase (foodops.counter.app a00083c..2e88679, main; SyncService.ts, SyncStatusListener.tsx, FailedSyncSection.tsx, plus new poisonedCustomerRecovery.test.ts — within this entry's tracked src/offline/** and src/components/offline/** surface, which underlies the documented Offline behavior section). NEW OFFLINE-RESILIENCE CAPABILITY, not previously documented and not contradicting any existing claim: a customer created offline can end up 'poisoned' — surviving locally with no serverId and no route to sync (root-caused to a since-fixed clearSessionData gap; incidents named Shawarmashi 2026-07-24, Thoum Express 2026-07-28). Previously, any order selecting such a customer was permanently blocked (cascading a 'parentTempIds' dependency failure before any HTTP call, with Retry unable to help since the parent could never be created). Two new defenses: (1) when an order create is blocked only by an unresolvable customer parent, SyncService now re-points the order to the cached Walk-In customer (dropping any temp delivery address that hung off the lost customer) so the order and its payments still reach the server, and SyncStatusListener shows a new toast — 'An order was synced as Walk In' / 'Its customer could not be saved to the server... Re-select the customer on the order if it does not reappear.' — the customer is re-attached automatically if it later resolves; (2) a new 20s-after-init startup sweep, healUnresolvableLocalCustomers, proactively repairs any already-poisoned local customers found on a device (reconciling a landed id-mapping, or re-enqueuing a missing /customers create row, letting the existing duplicate-customer 400 recovery path map it to its real server id). Separately, a generic failed-sync-item retry error toast changed copy ('Item is no longer retriable' → 'Retry cannot fix this one — see the error for what to do') to point the cashier at the row's own error detail; this touches FailedSyncSection.tsx, which this entry does not currently describe. None of this changes the documented Cash/Transfer/Card payment walkthrough, the Order Lifecycle table, the tempId→serverId swap, or the offline/sync indicator's mounting (SyncStatusListener remains wired up in main.tsx per the already-tracked claim, now handling one more event). Worth a line in Offline behavior or Common Questions ('what happens if a walk-in-eligible customer I picked offline can't sync?') if this entry is ever expanded to cover customer-selection edge cases during offline order creation; no edit needed to existing prose since nothing currently written is contradicted. (auto-applied by the truth pipeline; adversarially gated)
2026-07-31 — codebase (foodops.counter.app 5468530..5ac2982, main; BMLQRCodeModal.tsx, paymentService.ts, POSPage.tsx, bootstrapFallbacks.ts — within this entry's tracked orders/, payments/ and offline/** surface). Refines, does not contradict, the 2026-07-22-logged Online/BML QR payment mechanism (still not written into this entry's main prose): the modal previously required a server-hosted
qrCodeUrlimage and errored ('Failed to generate the payment QR code') without one; it now renders the QR client-side (viaqrcode.react) from apaymentUrlBML payment link — the format BML actually returns — falling back toqrCodeUrlonly if a hosted image is ever supplied. This looks like a correctness fix for the Online tender path rather than a new capability; the cashier-visible result ('Scan QR Code to Pay') is unchanged. Two unrelated, out-of-scope changes in the same range: (1) POSPage's SignalR handling now scopes subscriptions to the current session (sessionId) and, for status-only pushes already mirrored into IndexedDB, repaints from local cache (localOnly: true) instead of forcing a network refetch — a performance optimization with no change to any documented step or label; (2) newbootstrapCache.saveSessionCloseSummary/loadSessionCloseSummaryplumbing supports an upcoming session-close-summary print feature, which belongs to sales-sessions.md if ever documented, not this takeaway-order entry. No edit needed to existing human-curated sections. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-07-29 — codebase (foodops.counter.app c7d7801..c4cf5dc, main; OrderListItem.tsx, OrderCard.tsx — within this entry's tracked orders/** surface). NEW USER-FACING CAPABILITY, not previously documented: NEW orders (orderStatus 'new' / restaurantOrderOperationStatusId===1) now get inline Accept/Reject actions in the order-list row (OrderListItem), mirroring OrderCard. Accept posts to /orders/mark-accepted ({OrderId}) and optimistically flips the row to 'accepted' via a new onOptimisticStatusChange callback (wired in POSPage); Reject opens an in-app confirmation dialog and, on confirm, reuses the existing cancel path (/orders/cancel with CancelRemarks: 'Rejected by counter'), optimistically removing the row. Accompanying design cues: a new order arriving from the ordering app (isMobile && not cancelled) gets a blinking red border overlay on OrderCard (z-30, drawn above the existing NEW ribbon per the diff's own comment), the list row shows elapsed time in red for any new order, and the print button now sits after the action buttons. Separately, the status-pill text for orderStatus==='new' in OrderListItem changed from 'New' (emerald) to 'Pending' (gray) — this doesn't contradict this entry's Order Lifecycle table (which documents lifecycle-stage semantics, not literal pill copy, and OrderCard's 'NEW' ribbon appears unchanged per the adjacent comment), but is a watch-item if this entry is ever expanded to describe order-list pill styling directly. None of this touches the documented takeaway walkthrough (Steps 1-4), Pay & Close, or offline behavior. Worth a short addition on Accept/Reject for incoming orders (most relevant to delivery/ordering-app orders reaching the counter) if this entry or a dedicated incoming-orders page is expanded; no edit needed to existing human-curated sections since nothing currently written is contradicted. (auto-applied by the truth pipeline; adversarially gated)
2026-07-23 — codebase (foodops.counter.app dea0b59..61f13d1, main; OrderDetailsPanel.tsx — within this entry's tracked orders/** surface). NEW USER-FACING CHANGE, not contradicting any documented claim: on the order-details/payment card, the 'Change:' line (shown when a cash overpayment produces a changeBase > 0.01) is no longer a single amount in the order's own currency — it now renders the same change amount converted and listed per enabled currency (iterating
enabledCurrencies, converting viarateFor(c.id), falling back to the order's own currency/symbol when no multi-currency config is enabled). This is the first confirmed UI surfacing of the per-tendercurrencyId/multi-currency groundwork flagged as backend-only in the 2026-07-10 verification entry ('No currency-selector UI is visible in this diff excerpt... Worth a watch-item... once UI evidence... surfaces') — that watch-item can now be considered partially resolved, though this is the change-display, not a tender/currency selector on the payment-entry side. This entry's Step 4 payment walkthrough and 'Cash: pre-fills the full amount, editable for exact change' line aren't contradicted (cash entry/change math is unchanged), but neither currently mentions that change can display in multiple currencies. Worth a line in Step 4 or Key Concepts if multi-currency tendering becomes a documented cashier-facing feature; no edit needed yet since no currency-selector or cashier-facing multi-currency tender flow is confirmed by this diff alone. (auto-applied by the truth pipeline; adversarially gated)2026-07-22 — codebase (foodops.counter.app 85feff4..dea0b59, main; orderActions.ts, diffKot.ts, buildKot.ts, mergeKotItemStatus.test.ts — within this entry's tracked src/offline/** surface). NEW INTERNAL BUG FIX (SO-6662), not a change to any documented walkthrough step: editing an already-fired order (adding/changing line items after the initial KOT was sent to the kitchen) generates a 'modification' kitchen ticket via a client-side diff (diffKot) comparing the previous KOT baseline to the current line items. A prior bug — when the same order line ended up recorded on two locally-merged KOT tickets under divergent ids (a local/server id-pairing miss, per the new mergeKotItemStatus.test.ts regression test) — could double-count that line's previous quantity, so adding an unrelated item could fabricate a phantom 'Qty from N to M' decrease on a line nobody touched. The fix threads the order line's own authoritative prior quantity (
prevLineQuantityById, keyed by salesOrderLineItemId, computed from the pre-edit payload inupdateOrder) intodiffKot, which now caps the previous-KOT summed quantity at it — never inflating, only preventing an over-count — so a genuine decrease still fires (regression-tested) but a doubled baseline can no longer manufacture one. This entry's Order Lifecycle and Offline behavior sections don't currently describe order-editing-after-KOT-send or the kitchen modification-ticket mechanism at all, so nothing existing is confirmed or contradicted — but the underlying feature (editing a fired order re-diffs and re-sends a modification KOT, with 'Qty from N to M' cashier/kitchen-visible change notes) is a previously-undocumented capability worth a line if this entry or a kitchen-ticket-focused entry is ever expanded to cover post-acceptance order edits. (auto-applied by the truth pipeline; adversarially gated)2026-07-22 — codebase (foodops.counter.app 69b3494..85feff4, main; OrderDetailsPanel.tsx, BMLQRCodeModal.tsx, paymentService.ts — within this entry's tracked orders/** and payments/** surface). TWO NEW USER-FACING CHANGES, neither contradicting a documented claim. (1) A fourth settle-screen payment option — 'Online' (BML Connect QR) — now exists alongside the documented Cash/Transfer/Card. When any selected payment method has
requiresGatewaytrue, Pay/Pay & Close no longer settles synchronously: it hands the whole intended split (cash + the single Online tender) to a new BMLQRCodeModal, which callspaymentService.createOnlinePayment()to get a QR the diner scans in their BML app, then pollsgetOnlinePaymentStatus()every 3s until a terminal state; on CONFIRMED the server has already settled and closed the order atomically (per code comment, ADR-0013), andonPaidrefreshes the panel. This doesn't falsify Step 4's 'Click Cash, Transfer, or Card' (all three still work exactly as documented) — it's an additional option this entry's payment-method list and Common Questions don't yet cover. (2) Cancel Order is now permission-gated: the cancel-eligibility check (previously just 'not locked && not cancelled') now also requiresuserPermissions?.canDeleteSalesOrders === true, denying by default when permissions are unknown/offline-cold (comment: 'avoids a queued cancel the server would 403'). This doesn't contradict the Common Questions line that 'cancellations of accepted orders require a reason' — it adds a prerequisite (permission) this entry doesn't mention. Worth adding: a fourth bullet/Q&A for the Online/BML payment flow in Step 4 and Common Questions, and a permission caveat on the cancel Q&A — but nothing currently written is wrong. (auto-applied by the truth pipeline; adversarially gated)2026-07-16 — codebase (foodops.counter.app 30e5ffe..50c4771, main; SyncService.ts — within this entry's tracked src/offline/** surface). Two changes, neither contradicting a documented claim. (1) NEW INTERNAL TIMING CHANGE: handleOnline no longer calls scheduleProcessing(0) on reconnect — it now applies full jitter, starting the queue-drain at a random point in a 0-30s window (Math.random() * 30000), so a whole fleet in one timezone doesn't all drain their sync queue in the same second after a regional network blip; user-initiated syncs (sync-queue-updated) stay immediate. This entry's Offline behavior section only says the POS 'syncs the pending order to the server' once connectivity returns, without specifying timing, so nothing is contradicted — just a longer worst-case delay (up to 30s) before a reconnect-triggered sync visibly begins. (2) NEW FEATURE-ADJACENT PLUMBING, out of this entry's scope: SyncService now also re-targets unifiedOfflineDB.billSplits/billSplitParts rows from tempId to serverId during the same order-id migration as orders/payments, and make-split-payment sync responses are handled distinctly — a split-part payment is marked paid optimistically at tap time, and if the server's HTTP-200-with-isSuccess:false rejection comes back, the part is reverted (isPaid:false, syncStatus:'failed') and a
bill-split-payment-rejectedevent fires, preventing a rejected split from cascading into a phantom fully-paid/closed order. This is bill-SPLITTING (dividing one order's total into N separately-payable parts), which is distinct from this entry's documented 'cash + card combined' Q&A (multiple tenders applied to one undivided settlement). Per this entry's own Common Questions ('that's a dine-in workflow, not takeaway... Dine-in supports running tabs'), bill splitting reads as a dine-in/group feature — if ever documented, dine-in/tables.md is the more likely home, not this takeaway walkthrough; no edit needed here. The accompanying RCU-019/020 test additions (flushReceipt no-op guard, isRealPaymentId coverage) only add test coverage for the isRealPaymentId() guard already logged in the 2026-07-13 verification entry — no new behavior. (auto-applied by the truth pipeline; adversarially gated)2026-07-13 — codebase (foodops.counter.app 0545649..30e5ffe, main; SyncService.ts — within this entry's tracked src/offline/** surface). NEW INTERNAL CORRECTNESS FIX, not a cashier-visible change to any documented step: the salesmade checkout-sync listener (entityType 'salesmade-checkout') now guards the paymentId it persists/forwards with a new isRealPaymentId() check. A credit/no-payment checkout (e.g. Transfer-on-account) returns PaymentId = Guid.Empty ('00000000-0000-0000-0000-000000000000') — a truthy string that was previously stored/forwarded as-is and would 404 ('Payment not found') if a stashed receipt tried to attach to it via the salesmade receipt-upload path. It's now normalized to null so the stash is retained instead of erroring or being silently lost (covered by new tests RCU-005a and RCU-008a). This 'salesmade checkout'/receipt-upload-stash mechanism is not currently described anywhere in this entry's prose — Receipt Options only documents the manual Print Receipt/Tax Invoice/Email/Share actions — so nothing existing is confirmed or contradicted. The rest of this diff range (wasOffline/clientOccurredAt stamping on order/payment sync in orderActions.ts and paymentActions.ts, the ORD-006 wall-clock de-flake, per-tender currencyId/hasPerTenderCurrency groundwork, and isQuoteModeEnabled snapshot persistence in sessionActions.ts) reproduces findings already logged in the 2026-07-08 and 2026-07-10 verification entries — no new behavior there. No edit needed to the human-curated sections; worth a line if this entry is ever expanded to cover the salesmade checkout/receipt-upload flow. (auto-applied by the truth pipeline; adversarially gated)
2026-07-10 — codebase (foodops.counter.app 0545649..b3821b1, main; paymentActions.ts — within this entry's tracked src/offline/** surface). The wasOffline/clientOccurredAt offline-origin stamping (now also present on queuePaymentUpdate and retryPaymentsForOrder) and the sessionActions.ts isQuoteModeEnabled snapshot field are the same findings already logged in the 2026-07-08 and 2026-07-10 verification entries — no new behavior there. The one hunk not previously captured: PaymentMethod gained an optional per-tender currencyId (server Currency.Id; undefined = Base, tenderedAmount remains NATIVE) — groundwork for ADR-0011 multi-currency tendering. When any selected payment method carries a currencyId, makePayment now skips the existing capNonCashTenders overtender-cap (hasPerTenderCurrency guard), because that cap compares a NATIVE tendered amount against a BASE-currency outstanding — unit-mismatched for a foreign tender — deferring instead to the server's own TenderMath clamp. No currency-selector UI is visible in this diff excerpt, so this reads as backend/data-model groundwork rather than a confirmed cashier-facing capability yet; nothing in this entry's documented Cash/Transfer/Card walkthrough or the 'cash + card combined' Q&A is contradicted. Worth a watch-item for Key Concepts / Common Questions once UI evidence of a currency selector on the payment screen surfaces; not actionable yet for the human-curated sections. (auto-applied by the truth pipeline; adversarially gated)
2026-07-10 — codebase (foodops.counter.app 0545649..e4e7aaa, main; sessionActions.ts, SyncService.ts, orderActions.ts, paymentActions.ts — within this entry's tracked src/offline/** surface). The wasOffline/clientOccurredAt offline-origin stamping on order/payment sync and the SyncService.toUpdatePaymentForm multipart carry-through are unchanged from the already-logged 2026-07-08 finding — this wider diff range adds no new behavior there, just more test coverage. The one genuinely new hunk in this range is unrelated to takeaway orders: OfflineSessionSnapshot (sessionActions.ts) gained a persisted isQuoteModeEnabled flag (tests SES-055/056) so a hard refresh — which rebuilds currentSession from this offline snapshot rather than the live /sessions/my DTO — keeps a per-register 'Quote' button visible instead of losing it. That's a Quote-Mode/session-lifecycle concern, not part of this entry's documented Takeout/Pay & Close walkthrough; if worth capturing anywhere, it belongs in sales-sessions.md or a quotes-specific entry, not here. Also in-range: the ORD-006 test de-flake (wall-clock assertion swapped for the existing syncing:true assertion, no behavior change — already noted 2026-07-08) and RCU-016/017/018 salesmadeReceiptUpload test refactors, which only differentiate error-message branches (backend message vs axios-style error vs non-Error throw fallback) with no change to stash/retry behavior. No cashier-visible claim in this entry is touched, confirmed, or contradicted by this range; no edit needed to the human-curated sections. (auto-applied by the truth pipeline; adversarially gated)
2026-07-08 — codebase (foodops.counter.app 0545649..6885e57, main; SyncService.ts, orderActions.ts, paymentActions.ts — within this entry's tracked src/offline/** surface). NEW INTERNAL SYNC METADATA, not a cashier-visible change: order create/update, payment create, payment-update queuing, and orphaned-payment retry now all stamp their queued sync payload with
wasOffline(fromofflineGate.isOffline(), reflecting navigator.onLine at operation time — 'device reported offline', not 'API unreachable') andclientOccurredAt(an ISO timestamp taken from the record's local createdAt, or current time for update/queue paths). ForretryPaymentsForOrderspecifically,wasOfflineis hardcodedtruesince those payments were orphaned by a parent-order conflict and are inherently offline-origin.SyncService.toUpdatePaymentFormwas extended to carry both fields into the/orders/update-paymentmultipart body alongside the existing fields. This is purely additional audit/origin metadata sent to the backend for orders and payments that were already offline-sync-capable (per this entry's documented tempId→serverId flow and the previously-noted offline payment-edit capability) — it does not add a new cashier-facing capability, change any button/label, or touch the create/queue/sync control flow itself (confirmed by the accompanying test changes, which only assert the new fields are present in the queued body, and the ORD-006 de-flake, which just swaps a wall-clock assertion for the existingsyncing: trueassertion with no behavior change). No edit needed to this entry's human-curated sections; useful only as background if the Offline behavior section is ever expanded to describe what the server receives about offline-originated actions. (auto-applied by the truth pipeline; adversarially gated)2026-07-06 — codebase (hubits-api-sales 6f242ad..e31810a, SessionsController.cs — within this entry's tracked Controllers/SessionsController.cs surface). NEW BACKEND BEHAVIOR, not a change to any documented takeaway-order step: register-session lifecycle actions (Open Register, session Close, cancel-close, Adjust) now each write an operation-log entry via TouchCreate/Touch ('Register opened.', 'Session closed. ...', 'Session close cancelled. ...', 'Session adjusted. ...'), and a new GET Sessions/{sessionId}/history endpoint returns the parsed audit timeline (action label, actor, timestamp, per-field diffs from OperationLog.ExtraDetails). Nothing in this diff changes the cashier-visible Open Register / denomination-count / close-and-reconcile flow this entry documents in Step 1 and the 'How do I close out at end of day?' Q&A — no response shape change is shown for those existing endpoints, only an added audit-log side effect. This is primarily relevant to session lifecycle documentation (../sales-sessions.md) rather than this takeaway-order walkthrough, and there is no frontend evidence in this diff that a session-history UI consumes the new endpoint yet, so no cashier-facing claim here is confirmed or contradicted. Worth a cross-reference note in sales-sessions.md's session lifecycle section if that entry gets expanded; no change needed to this entry's human-curated sections. (auto-applied by the truth pipeline; adversarially gated)
2026-07-05 — codebase (foodops.counter.app 9200293..0545649, main; SyncService.ts and offline/database.ts — within this entry's tracked src/offline/** surface). NEW OFFLINE-SYNC BUG FIX, not previously documented: a new backfillMarkStatusItemIds() resolves client-local
temp-<uuid>line-item ids before sending the kitchen-status endpoints (/orders/mark-ready, /orders/mark-served, /orders/mark-preparing) for an order that was created offline — previously these calls 400'd because handleSuccess only ever mapped the order's own temp id on create-sync, never its line items (root-caused and reproduced in the new markStatusTempIdRepro.test.ts). The fix re-fetches the now-synced order and maps each temp- line to its real server line id by productVariantId + ordinal, or defers the status change until the parent order has synced — it never sends a 400-bound or mis-mapped body. Separately, clearAllCache() (invoked by the POS's 'Reload Cache' button, already documented in this entry's Key Concepts) now also clears the syncMeta table, which holds the salesmade catalog delta-sync cursor; previously a cache reload cleared cached products but left the cursor in place, so the next delta sync returned nothing and the POS grid stayed empty ('No products yet') until a harder reset. Neither change touches a cashier-visible label or the documented takeaway walkthrough steps, and neither contradicts any existing claim (Order Lifecycle statuses, tempId→serverId swap, Reload Cache behavior all still hold) — but both are genuine correctness fixes to already-documented offline/cache mechanics and worth a line in Key Concepts (Reload Cache) or the Offline behavior section if this area is expanded. The same range's modifier-dialog width/label and OrderItems.tsx icon-font swap (Minus/Plus/Trash2 → hgi icon classes) are purely cosmetic and out of this entry's scope (modifiers UI is not documented here; the -/qty/+ and delete controls remain functionally unchanged). (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-07-XX — codebase (hubits-api-sales 01cd718..3005390, OrdersController.cs CancelOrder — within this entry's tracked Controllers/OrdersController.cs surface). NEW BACKEND BEHAVIOR, not a UI change: cancelling an order now calls
order.ReverseBomStockDeduction(_dbContext, order.Id), restoring recipe/BOM-linked ingredient stock that was deducted (per the already-documented Pay & Close BOM deduction in this entry's Key Concepts) — a legacy-parity fix, wrapped non-fatally in try/catch and logged on failure so a reversal error doesn't block the cancel itself. TheCancelOrdersuccess response also now carries a diagnosticBomStockReversalLog(OrderId, ReversedLineItems count, Restored list) — an internal/diagnostic field, not a cashier-visible label. This complements, and does not contradict, this entry's existing note that 'paying out a takeaway order with recipe-linked items triggers ingredient stock deduction server-side' and the Common Questions line that 'cancellations of accepted orders require a reason' — it fills the previously-undocumented gap of what happens to already-deducted stock on cancel (it's now restored). Worth a short addition to Key Concepts (BOM deduction ↔ reversal pair) if inventory/stock behavior is ever expanded in this KB, but no change to any cashier-facing walkthrough step, button, or label. The commit range's other subject ('Day Summary exports through tray pipeline with legacy workbook') touches reporting, not orders, and falls outside this entry's scope. (auto-applied by the truth pipeline; adversarially gated)2026-07-04 — codebase (foodops.counter.app be4fe21..d439ab4, main; OrderDetailsPanel.tsx and SyncService.ts — within this entry's tracked orders/** and offline/** surface). Beyond the already-documented offline payment-edit branch (handleSavePayment isOffline path, noted in the prior 2026-07-04 entry), this range adds: (1) a best-effort useEffect that warms a customer's loyalty-program enrolments into cache while online (cache-first, 4h TTL/SWR) so the Apply Loyalty modal's dropdown still works offline — a new, undocumented loyalty feature area, not part of this entry's takeaway walkthrough; (2)
mirrorLocally()is now also called on the ONLINE successful payment-edit path (previously only mirrored locally when offline), keeping the local IndexedDB payment record consistent with server-confirmed edits — internal consistency fix, no cashier-visible change; (3)settlementMath.tsgains acapNonCashTendersexport and a newstaleTotalGuard.tsservice (fetchServerOutstanding,isOutstandingStale) are imported into OrderDetailsPanel — these read as a client-side counterpart to the backendNonCashOverTenderTolerance/stale-outstanding-amount handling already noted in this entry's history, but no call site is visible in this diff excerpt, so the actual behavior is unconfirmed pending a follow-up diff. Separately,SyncService.tsgained two client-resilience mechanisms unrelated to any documented walkthrough step: a 20s leader-recontest cadence so a stranded idle tab (e.g. an always-on KDS display) can take over the sync queue if the leader tab dies, and an 'ingress-orphan sweep' that, after a 10-minute age gate, flags local entities that were written but never enqueued for sync (a crash in the write→enqueue window) into the existing/conflictsfailed lane, plus a neworderMarkAcceptedentity type added to that sweep's scope. None of this contradicts any documented claim (Pay & Close, tempId→serverId swap, print-broker decoupling, offline indicator all untouched in this diff); it's additional background/internal robustness plus an unrelated loyalty feature, worth a follow-up check oncapNonCashTenders/staleTotalGuardusage but not yet actionable for the human-curated sections. (auto-applied by the truth pipeline; adversarially gated)2026-07-XX — codebase (foodops.counter.app be4fe21..1266a30, src/components/orders/OrderDetailsPanel.tsx handleSavePayment). NEW USER-FACING CAPABILITY, not previously documented: editing an existing payment on an order (method, amount, reference number, cheque number, remarks) now works while offline, not just online. The edit is mirrored into local IndexedDB immediately (so the receipt/breakdown reflect it without a refetch) and queued via queuePaymentUpdate for sync once connectivity returns, surfacing a distinct toast ('Payment updated — will sync when online') instead of the online-path 'Payment updated successfully' toast. One gated edge case: if the cashier tries to attach a new receipt file to the payment edit while offline, the save is blocked with 'Reconnect to attach a receipt' (a File can't sit in the offline sync queue) — everything else in the edit form is still editable offline. This entry currently has no section describing payment editing at all (only initial Cash/Transfer/Card settlement and cash+card combos are documented in Common Questions); worth adding a short 'Editing a payment' subsection or Q&A entry, noting the offline queuing behavior and the receipt-attachment exception. Separately, unrelated internal robustness changes landed in src/offline/SyncService.ts this same diff (per-order wave serialization via a new orderKeyForItem canonicalization to avoid spurious 409s when a payment and an order-mutation for the same order race in the same sync wave, plus a leader-tab re-contest timer) — these are non-user-facing sync-queue correctness fixes and don't change any documented cashier-visible behavior. (auto-applied by the truth pipeline; adversarially gated)
2026-07-04 — codebase (hubits-api-sales 30a126a..aa4a8d5, PaymentProcessingService.cs — within this entry's tracked Payment/** surface, beyond the already-verified 7b2a1a9 BOM-deduction pass). NEW BACKEND BEHAVIOR, not a UI change: a small rounding tolerance (
NonCashOverTenderTolerance, default MVR 0.05, configurable viaSales:NonCashOverTenderTolerance) was added to both the full-order and split-payment Pay & Close paths. Previously any non-cash (Card/Transfer) tender exceeding the order or split-part total by even a cent hard-failed with "Total/Non-cash payment amount exceeds the order total"; now tenders within the tolerance are accepted (a warning is logged citing the delta) and are still clamped to the order total by the existingCreatePaymentMath.Min, so nothing over-posts — the overage stays visible onPayment.TenderedAmount/CashBackAmount. This accommodates compound-tax rounding when an offline-computed order total drifts a cent or two from the server's. It's cents-scale and doesn't change any cashier-facing label or the walkthrough steps, but worth a line in Key Concepts if the KB ever needs to explain an occasional accepted card/transfer overpayment of a few cents on Pay & Close. (The same diff range also carries the StockProductVariantSerialId/BatchId/StockLocationId passthrough and BOM stock deduction already captured in this entry's prior 2026-07-04 verification note — not new — and unrelated SessionsController.cs org-local timezone/date-filter and session-totals changes to the session-history/reporting endpoints, which fall outside this entry's documented claims and belong, if anywhere, in sales-sessions.md.) (auto-applied by the truth pipeline; adversarially gated)2026-07-04 — codebase (foodops.counter.app be4fe21..31f5c7a, main; OrderDetailsPanel.tsx — within this entry's tracked orders/** surface). NEW OFFLINE-EDITING BEHAVIOR, not previously documented: OrderDetailsCard.handleSavePayment now branches on isOffline — editing an already-recorded payment (method, amount, reference/cheque number, remarks) while offline mirrors the edit locally (editPayment) and queues it for sync (queuePaymentUpdate), surfacing toast.success('Payment updated — will sync when online') instead of failing or blocking on connectivity. A receipt-file attachment on the edit is explicitly disallowed offline (toast.error('Reconnect to attach a receipt')) since a File object can't sit in the sync queue. This extends this entry's existing Offline behavior section (which currently covers only order creation/tempId→serverId swap, print-broker decoupling, and stale-while-revalidate caching) — post-Pay&Close payment corrections are now also offline-capable. Worth a line in Offline behavior / Key concepts, and possibly a new Common-questions entry ('Can I edit a payment while offline?'). Does not contradict any existing claim in the entry; no cashier-visible label/button in the documented walkthrough changes. (auto-applied by the truth pipeline; adversarially gated)
2026-07-04 — codebase (hubits-api-sales 30a126a..7b2a1a9, PaymentProcessingService.cs — within this entry's tracked Payment/** surface). NEW BACKEND BEHAVIOR, not a UI change: both the full-order and split-payment paths in Pay & Close now call
salesOrder.ProcessBomStockDeduction(_dbContext, salesOrder.Id)immediately before invoice creation, deducting recipe/BOM-linked ingredient stock at the moment payment is processed; the call is explicitly idempotent per line item (comments cite the 'Kit path in ProcessStockTransactions' double-deduct guard and split-payment 'deducted once on the first split part'). Invoice line items created from the order also now carryStockProductVariantSerialId/StockProductVariantBatchId/StockLocationIdpassthrough. None of this changes any cashier-visible label, button, or screen documented in this entry's walkthrough — Pay & Close still behaves as described — but it's worth a line in Key Concepts (or a cross-link to menu/inventory docs) that paying out a takeaway order with recipe-linked items now triggers ingredient stock deduction server-side, useful context if stock/inventory KB pages ever need to explain a post-sale stock drop. (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). UPHELD: tax is strictly per-line (orderCalculationService.calculateOrderTotals— nodefaultTaxPercentageflat multiplier; the receipt "Tax (8%)" example is illustrative, not a session rate); order reference still server-issued at create viaGetAndUpdateNextNumber(SalesOrders)(OrderCreationService.cs:86,248), so the offline tempId → serverId swap holds; Pay & Close atomic payment + close inOrderDetailsPanel. CORRECTIONS: (1) the 2026-05-04 "no offline indicator" finding is SUPERSEDED — that was the retired legacyapp.foodops/orders/indexshell; the new counter PWA shipsOfflineIndicator.tsx+SyncStatusBadge.tsx/SyncStatusListener.tsx(mounted inmain.tsx); flagged the old section as legacy. (2) Both "Known Issues" (jQuery modal "items not showing",/menucategories/edit/{id}Razor edit) flagged as legacy-monolith artifacts retired at launch. (3) Legacy/ordersroute note corrected to the counter app's/counter+/sessions/:id/pos. (4) Added a note that order save is decoupled from the local print broker (queue + idempotent replay) — confirmed inprintActions.ts. (5) Droppedhaafai.app.foodopsfrom frontmatter. Frontmatter SHAs + last_verified bumped. Residual: the human-curated walkthrough labels/screenshots predate the counter PWA and warrant a live re-walk on counter.salesmade.io.
Warning: Code-verified 2026-05-03 · codebase only (auth.json stale) Verified_by:
codebase· Last verified: 2026-05-03 Offline behavior section derived from commitsa4f0488,244ef17,68e0bf7(haafai/foodops.counter.app). Visual offline indicator UI marked with<!-- UI-TODO -->for re-verification after auth.json refresh. All other prose unchanged from prior playwright pass; treat exact UI labels as indicative until re-verified.
Note: UI-verified 2026-05-04 · playwright offline injection ([email protected]) Verified_by:
playwright· Last verified: 2026-05-04 Offline mode confirmed at network layer (SignalR disconnection, ERR_INTERNET_DISCONNECTED on all endpoints). No visible offline indicator UI observed at/orders/index— no banner, toast, badge, or disabled-state on action buttons. UI-TODO marker resolved: finding is absence of indicator, not a verification gap. Screenshots 177–180 captured inScreenshots/. See inline "Offline indicator: verified 2026-05-04" section for full details.
Note: Code re-verified 2026-06-04 · hubits-api-sales@5274891 + foodops.counter.app@e002374 (main) Verified_by:
codebase· Last verified: 2026-06-04 VERIFIED — NO USER-FACING CHANGE. The 2026-06-03 commit2489d1fcited in the refresh brief is "fix: let DB generate SalesOrderLineItemTax.Id when adding an item on order edit" — it is a backend logging/Id-generation tweak in theDbUpdateConcurrencyExceptionpath ofOrdersControllerorder-edit (+14/-1), NOT a change to the order/sale number. The order reference (SO-NNNN/YYYY) is still generated server-side at create time viaSequenceNumberTypeHelper.GetAndUpdateNextNumber(..., SequenceNumberTypes.SalesOrders, ...)and assigned tosalesOrder.Referencebefore save [code: hubits-api-sales/Hubits.Sales.Api/Services/Counter/Orders/OrderCreationService.cs:85-86, 248]. The offline-first tempId → serverId swap documented in the Offline section therefore still holds: a real reference number is issued when the offline order syncs online; the prior KB finding is unchanged (counter.app retains the tempId/serverId flow — 63tempId/ 59serverIdcode references on main). Takeaway is pay-at-counter with no table, so the new one-active-order-per-table gate does not affect this flow. No edits to the human-curated sections required.
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.