Foodops
All docs

Guides · Updated 2026-08-23 · vd40b18a

Close a Paid Order

Audio narration for this article is generated with the weekly docs refresh.

Plain-English summary

In Foodops, orders and register sessions have separate lifecycles — each with its own concept of "closed." A fully-paid order shows status Closed in the Sales Orders tab; nothing further is required on that order. The register session that contains the order stays open so the cashier can serve the next customer. "Pay & Close" on the payment modal is a per-order action; "Close Register" at end of shift is the session-level action. Seeing Closed (fully-paid) orders in a session that is still Open is normal, correct operation — not an error state requiring cashier intervention. (Both an order and a session can read "Closed" — an order is Closed once paid; a session is Closed only after end-of-shift Close Register + approval.)

When you'd use this

  • A cashier sees an order with "Paid" status in Session Details and wonders whether they need to do something else to "finish" it
  • A support conversation asks "the order is paid but the register is still open — is something wrong?"
  • Training — explaining to new cashiers that Pay & Close on the cash payment modal does NOT close the till
  • Troubleshooting — an order appears in the Sales Orders tab with an unexpected status after payment and you need to understand what state it is actually in

Two levels: order lifecycle vs session lifecycle

Foodops has two independent lifecycles that both use the word "close" and are easy to conflate:

Level What it tracks How it closes
Order lifecycle One customer ticket — items, payment, receipt Pay & Close button in the payment modal
Session lifecycle One shift on a register — all orders, cash, reconciliation Close Register in Session Details, at end of shift

Closing an order does not close the session. A session closes once, at end of shift, after all the day's orders are done.

What "Pay & Close" does

When a cashier taps Take Payment on an active ticket and selects Cash, the cash payment modal opens. At the bottom of that modal is a Pay & Close button.

Tapping Pay & Close performs two actions in a single submission:

  1. Records the payment — marks the order fully paid and its status Closed, computes change from the amount received, writes a payment receipt to the session's Receipts tab.
  2. Closes the order ticket — removes it from the active ticket panel on the Counter screen.

After the submit succeeds, a receipt confirmation modal appears. The cashier can print or email the receipt from there. Dismissing the receipt modal clears the ticket and opens a new blank ticket on the same open session. The session remains open.

There is no separate "close order" step after Pay & Close. The order is done at that point. The session continues so the next customer's order can be taken.

For non-cash payment methods (card, on-account, loyalty), the flow differs in presentation but the outcome is the same: once payment is fully captured, the order is fully paid and its status moves to Closed as the ticket clears.

What a fully-paid order's "Closed" status means

When a user opens Session Details and navigates to the Sales Orders tab, every order in the session is listed with a Status column. An order showing Closed is complete — payment was captured and the ticket was closed successfully. (A separate Paid indicator — a green "This order has been fully paid." checkmark — appears in the payment section of the Order Details panel; "Paid" describes the payment, "Closed" is the order status shown in the tab.)

Seeing ten "Closed" orders in a session that shows Open status is the normal state during an active shift. It means the cashier has processed ten customers and the register is still available for more. The session's Open status and the orders' Closed status are both correct simultaneously. (Don't confuse an order's Closed status with the session being Closed — the session only closes at end of shift via Close Register.)

When does a session move from Open to Closed?

Session status now follows a three-state machine: Open → PendingClose → Closed. A session does not jump straight from Open to Closed — a cashier-initiated close lands it in PendingClose, and a supervisor must then approve it to reach Closed. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:1592-1872 CloseRegister (sets status PendingClose at :1872), :1965 ApproveCloseRegister [HttpPost("approve-close")] @ main]

  1. From the Counter screen, tap Details on the active session → Session Details modal.

  2. Tap Close Register (button at the top-right of the modal).

  3. Count the physical cash by denomination on the Close Register form.

  4. Add a Comments note explaining any variance.

  5. Tap Close Register to submit. The session leaves Open and moves to PendingClose — this happens for every cashier close, whether or not there was a cash variance. (The success message reads "Register closed with variances. Approval may be required." when a variance is detected.) The register is not yet free for the next shift at this point.

  6. A supervisor/manager runs Approve Close (the approve-close action) on the PendingClose session. Only then does it transition to Closed, recording the approver and approval time, and the register becomes available for the next shift. The Approve Close action surfaces in two places on the new stack:

    • At close time (fast path) — if the count doesn't match, the cashier immediately gets the Balance Difference Warning dialog ("The closing cash count does not match the expected balance.") with Close Anyway (= approve, straight to Closed) and Cancel buttons. A close approved this way never parks in PendingClose from the user's perspective.
    • Counter app (deferred path)Register Sessions (/register-sessions, register-sessions rail icon) lists every session with status pills (Open / Pending Close / Closed); opening a Pending Close session shows an Approve Close button and an Adjustment button on its details page, alongside Closing Count and Reconciliation tabs.

    The legacy web app (app.foodops.io Session History at /salessessions) that previously also exposed "Approve Close Register" is retired at the July-10 launch — the counter app's Register Sessions is now the only deferred approval surface.

    Counter app — Pending Close session details with Approve Close and Adjustment buttons

This is unrelated to individual order status. The session can be closed whether the orders inside are Closed (paid) or in progress — although best practice is to resolve any outstanding orders before closing.

Note: Previously a no-variance close went straight to Closed; that branch was removed so the supervisor-approval step is never skipped — CloseRegister now sets SalesRegisterSessionStatuses.PendingClose on every close (SessionsController.cs:1872 @ main). The SignalR/notification label still distinguishes "PendingClose" vs "Closed" by variance (:1885, :1924), but the persisted session status after Close Register is always PendingClose until approval.

Edge case: payment captured but order still appears open

In normal operation, the order's status moves to Closed immediately after Pay & Close succeeds. There is one scenario where a payment may be captured but the order does not transition cleanly: a split payment in progress (e.g., part cash, part card). The order remains open until all payment legs complete. Once the full amount is covered, the order is fully paid and its status moves to Closed.

If an order appears open after what should have been a complete payment:

  1. Check the Receipts tab in Session Details — if a receipt exists for the full order amount, the payment went through and the order may simply not have updated in the client view. Refresh the Counter page.
  2. If no receipt exists, the payment submit may not have completed. Re-open the order from the Sales Orders tab and re-process payment.

The Order Details modal (accessible via the orders list or session Sales Orders tab) shows a single Closed status badge (green pill) at the top when the order is fully paid and closed; a separate Paid indicator (green checkmark, "This order has been fully paid.") appears in the payment section on the right. Verified against the live app 2026-06-29 (re-verified 2026-07-05): status values in the Sales Orders tab include Closed (fully settled and closed), Open (ticket active, not yet paid), and Processing (accepted/ready but not yet paid — e.g. picked up awaiting payment). Note: the full-page /register-sessions/:id route is a different surface — it has a Transactions tab listing invoices (INV-xxxx, payment statuses like "Fully Paid"), not a Sales Orders tab; order statuses only appear in the Session Details modal.

Order Details modal showing Accepted + Paid + Closed status badges — 1× Coffee, MVR 48.60, Payment: Cash

Key concepts

  • Pay & Close — the single submit button in the cash payment modal. Atomically records payment and closes the order ticket. Per-order action. Does not close the register session.
  • Order status: Closed (in the Sales Orders tab) — the order is fully paid and the ticket is closed. No further cashier action required on this order. ("Paid" is a separate payment-section indicator, not the order's status label.)
  • Order status: Open — the order is in progress (items added, KOT sent, partial or no payment captured yet).
  • Session close — the end-of-shift action that counts cash and produces the variance figure. Accessed via Close Register in Session Details. It moves the session to PendingClose, not straight to Closed — a supervisor then approves it (Approve Close) to reach Closed and free the register for the next session. Independent of individual order status.
  • Session status: Open / PendingClose / Closed — the three session states. Open = register active, new orders can be taken (a session is Open even when all its orders are Closed/paid). PendingClose = cashier ran Close Register and counted cash; awaiting supervisor approval; register not yet reusable. Closed = approved and finalised; register available for the next shift. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:471-473 SalesRegisterSessionStatuses Open/PendingClose/Closed, :1414-1415 IsPendingClose/IsClosed DTO flags @ main]

Common questions

Q: An order shows "Closed" in the Sales Orders tab of Session Details but the register session is still Open. Do I need to do anything? A: No — this is normal. "Closed" in the Sales Orders tab means the order is fully paid and the ticket is closed. The register stays open until you close it at end of shift via Close Register. A session with many Closed orders and Open session status is the standard operational state.

Q: Is there a separate "close order" button I'm supposed to tap after Pay & Close? A: No. Pay & Close is the final step for a cash order. The receipt confirmation modal that appears afterward is informational — print or email the receipt and dismiss it. The ticket clears automatically, ready for the next customer.

Q: What if I accidentally close the payment modal before tapping Pay & Close? A: The order remains Open. Re-open it from the Session Details → Sales Orders tab, tap the Reference link to return to the ticket, and re-process the payment. If the customer has already left, check whether any receipt was generated before proceeding.

Q: Can I close the register while there are still Open orders in the session? A: Technically yes — the system does not block session close on unpaid orders. However, open orders will remain on the session record as unresolved, which creates an audit concern. Best practice is to cancel or complete all outstanding orders before running Close Register.

Q: The same order shows Closed in the Sales Orders tab but the cashier says the customer didn't pay. What do I investigate? A: Check the Receipts tab for the payment method and timestamp. If a cash receipt exists, the payment was recorded in the system — the cashier received cash and the drawer should reflect it. If no receipt exists, the status may be stale; refresh the page.

Q: How do I close a session that was left open overnight? A: From the counter app's Register Sessions list (/register-sessions), find the open session, open its details, then Close Register. Count the cash as it stands now and submit. This puts the session into PendingClose; a supervisor then approves it to move it to Closed. The Dashboard also shows a yellow warning toast when a session has been open for more than a day. See Sales Sessions. (The legacy /salessessions web-app list is retired at the July-10 launch.)

Q: I closed the register but it still shows as not fully closed / "pending" — is something wrong? A: No. Closing the register now moves the session to PendingClose, not directly to Closed — this happens on every close, even with no cash variance. A supervisor/manager has to run Approve Close to finalise it as Closed and free the register. Until then the session sits in PendingClose. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:1872 CloseRegister sets PendingClose; :1965 approve-close @ main]

Decision note — why this is a standalone entry

This entry exists because chat-agent's KB_AUDIT_2026-05-01 Issue #7 identified a 5-turn chat-agent loop caused by the absence of any KB answer to "order is paid but register still open." The concept spans two existing entries (sales-sessions.md owns the session lifecycle; counter-order.md in internal-docs owns the order-taking flow) without being clearly owned by either. A dedicated slug (close-paid-order) gives agent retrieval a direct hit rather than requiring the agent to find the right paragraph inside a longer document. If a future verification pass confirms the content would be well-served as a section within sales-sessions.md (for example, if the slug proves hard to discover), that migration can happen at draft-to-stable promotion time.

Related

  • Sales Sessions — the session lifecycle; Close Register is the session-level close

  • Sales Registers — the register is what a session runs against

  • Cash Handling — the cash events in a session, including the closing count

  • Session Handover — passing a session to another cashier; does not close any orders

  • 2026-08-23 — codebase (hubits-api-sales bb69312..0f25183, tenancy pin / cache-invalidation seam / order micro-cache / SignalR routing envelope / dependency bump / feat(customers) paged statements). Caveat on scope: SessionsController.cs is PATCH-UNAVAILABLE in this diff extraction (unlike prior passes over this same base range, which showed the file's hunks directly) — CloseRegister (:1592-1872), ApproveCloseRegister (:1965), the SalesRegisterSessionStatuses enum (:471-473), and the IsPendingClose/IsClosed DTO flags (:1414-1415) cannot be positively re-confirmed unchanged from this diff alone; no other file in this diff touches session-close logic, so there is no evidence of a contradiction, but the absence-of-evidence here is weaker than the 'shown-but-unrelated-hunk' confirmations logged on 2026-08-01 through 2026-08-12 for the same file. Everything else restates items already logged as NEW_INFO for overlapping ranges of this base: OrderPaymentController.cs's ExecuteUpdate cache-invalidation seam (2026-08-05) and reserve-before-processing payment idempotency gate (2026-08-01), and OrdersController.cs's SignalR IOrderEventPublisher routing-envelope refactor for order broadcasts (2026-08-07/08-12). One genuinely new item, out of scope for this entry's Pay & Close / session-close claims: OrdersController.cs adds POST /{orderId}/change-discount (ChangeOrderDiscount), letting a cashier apply a bill discount to an existing open order from the order-details view without rebuilding the whole order. It only ever ADDS a discount (never replaces/removes one, since nothing on a persisted order records whether an existing discount was auto-applied or cashier-entered) and refuses (200, Applied=false, with a Reason) rather than erroring — one guard shown is SalesOrderStatus.IsCancelled/IsCompleted, distinct from the 'Closed' Sales-Orders-tab status label this entry documents. This is a discount-application feature, not a change to Pay & Close settlement or the order/session status semantics this entry covers — worth a note in an order-discount doc if one exists, not this entry. No edit needed to this entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-23 — codebase (foodops.counter.app a3fe6a1..83f0da4, 'feat: reason lists per action type + move reason capture' / 'fix: record discarded lines at their true discounted amount' / three 'salesmade compact' rail-UI commits). Not a contradiction of any claim this entry makes: the only intersecting hunk is in OrderDetailsPanel.tsx's handleVoidBinCancel, and it doesn't touch the Pay & Close settle path, the Closed/Open/Processing order-status set in the Sales Orders tab, or (frontend-only diff) SessionsController's Open/PendingClose/Closed session machine. NEW_INFO, out of scope for this entry's Pay & Close/session-close claims but a further evolution of the void/cancel mechanism already logged as out-of-scope NEW_INFO on 2026-08-04 and 2026-08-07: the void-to-bin cancel action is now wrapped in a new quickUndoEngine (runQuickUndoable/quickUndoActive), gated on the session's quickUndo config. When a hold is already active for kind 'discard', the commit (voidOrderToBin + optimistic 'cancelled' status) still runs but the success toast is suppressed and the panel closes immediately (held ? 0 : 1000 ms) instead of after the 1s delay — implying the actual bin-move is now presented through a snackbar-driven undo affordance rather than an immediate toast, i.e. a brief undo window before the void is final. Terminology also shifts from 'void bin' to 'discard bin' in the toast copy. This belongs in an order-cancellation/void-bin doc, not here, and needs a live check (is enableVoidBin/quickUndo config on for any org yet, what the undo snackbar actually looks like, whether the order still shows in the Sales Orders tab during the hold) before documenting elsewhere. No edit needed to this entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-12 — codebase (hubits-api-sales bb69312..ecfea28, tenancy pin / cache-invalidation seam / order micro-cache / SignalR routing envelope / dependency bump / feat(customers) paged statements). Not a contradiction of any claim this entry makes or any tracked sidecar claim: CloseRegister (:1592-1872), ApproveCloseRegister (:1965), the SalesRegisterSessionStatuses enum (:471-473), and the IsPendingClose/IsClosed DTO flags (:1414-1415) are absent from this diff and unchanged — the only SessionsController.cs hunks shown are constructor DI additions (DaySummaryReportService/IHtmlToPdfService) and GetMySessionsAndRegisters (PosDefaultViewMode/SoftEnforcePaymentReference/enforceUniqueBlazeNumberForSessions flags plus the OpenedTime UTC-format fix), none touching the close/approve path. The reserve-before-processing idempotency gate and order-cache invalidation seam (OrderPaymentController), the SignalR IOrderEventPublisher routing-envelope refactor (OrdersController/OrderUpdateService), the outlet-aware payment-reference allocation and TenderMath.Book() tender-booking refactor (PaymentProcessingService/TenderMath.cs), the customer-SMS webhook move (CustomerOrderingApiNotificationService.cs), the bomReversals BOM-stock-restore list (OrderUpdateService), and the IDistributedIdempotencyStore.cs deletion all restate items already logged as NEW_INFO on 2026-08-01 through 2026-08-11 for overlapping ranges of this same base — no new claim there. One item is new to this diff, out of scope for this entry's Pay & Close / session-close claims: PaymentProcessingService.cs moves its post-payment stock-cache invalidation from two calls inside the settle transaction (pre-invoice-creation and post-invoice-approval) to a single post-commit call wrapped in try/catch, per the added comments 'Redis + SignalR round trips inside a transaction extend every lock it holds by external-system latency' and 'readers can repopulate the cache from pre-commit state' when invalidated pre-commit. This is a timing refinement of the same stock-cache-freshness mechanism already logged as NEW_INFO on 2026-07-29, not a change to when/whether an order reaches Closed status or a session moves through Open→PendingClose→Closed — worth a note in stock/inventory-facing docs if any exist, not this entry. No edit needed to this entry. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-12 — codebase (hubits-api-sales bb69312..b2ed34a, tenancy pin / cache-invalidation seam / order micro-cache / SignalR routing envelope / dependency bump / feat(customers) paged statements). Not a contradiction of any claim this entry makes or any tracked sidecar claim: CloseRegister (:1592-1872), ApproveCloseRegister (:1965), the SalesRegisterSessionStatuses enum (:471-473), and the IsPendingClose/IsClosed DTO flags (:1414-1415) are absent from this diff and unchanged — the only SessionsController.cs hunks shown are constructor DI additions (DaySummaryReportService/IHtmlToPdfService) and GetMySessionsAndRegisters (view-mode/soft-enforce-payment-reference/blaze-number flags, plus an OpenedTime UTC-format fix), none touching the close/approve path. The reserve-before-processing idempotency gate and order-cache invalidation seam (OrderPaymentController), the SignalR IOrderEventPublisher routing-envelope refactor (OrdersController/OrderUpdateService), the outlet-aware payment-reference allocation and TenderMath.Book() tender-booking refactor (PaymentProcessingService/TenderMath.cs), the customer-SMS webhook move (CustomerOrderingApiNotificationService.cs), the bomReversals BOM-stock-restore list (OrderUpdateService), and the IDistributedIdempotencyStore.cs deletion all restate items already logged as NEW_INFO on 2026-08-01 through 2026-08-11 for overlapping ranges of this same base — no new claim there. One item is new to this diff, out of scope for this entry's Pay & Close / session-close claims: on the Pay & Close settle path, PaymentProcessingService.cs's low-stock shopping-list background job (CheckAndCreateShoppingList) now takes an explicit orgId parameter alongside capturedOrderId — per the added comment, 'the Hangfire job has no ambient tenant, and the service scopes its dedupe and insert by it,' implying the prior call could dedupe/insert shopping-list items against the wrong org (or silently no-op) when the background job ran without request-scoped tenant context. This is an inventory/shopping-list correctness fix, not an order/session Closed-status change — worth a note in stock/inventory-facing docs if any exist, not this entry. Neither this nor any other item in the diff changes when/whether an order reaches Closed status or a session moves through Open→PendingClose→Closed, so no edit is needed to this entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-11 — codebase (hubits-api-sales bb69312..7273baf, tenancy pin / cache-invalidation seam / order micro-cache / SignalR routing envelope / dependency bump / feat(customers) paged statements). Not a contradiction of any claim this entry makes or any tracked sidecar claim: CloseRegister (:1592-1872), ApproveCloseRegister (:1965), the SalesRegisterSessionStatuses enum (:471-473), and the IsPendingClose/IsClosed DTO flags (:1414-1415) are absent from this diff and unchanged — the only SessionsController.cs hunk shown touches GetMySessionsAndRegisters, not the close/approve path. The reserve-before-processing idempotency gate (OrderPaymentController), the SignalR routing-envelope refactor (OrdersController/OrderUpdateService), the outlet-aware payment-reference allocation and TenderMath.Book() tender-booking refactor (PaymentProcessingService/TenderMath.cs), the customer-SMS webhook move (CustomerOrderingApiNotificationService.cs), and the bomReversals BOM-stock-restore list (OrderUpdateService) all restate items already logged as NEW_INFO on 2026-08-01/08-02/08-05/08-07/08-09 for overlapping ranges of this same base — no new claim there. Two items are new to this diff, both out of scope for this entry's Pay & Close / session-close claims: (1) GetMySessionsAndRegisters now also stamps each session with an enforceUniqueBlazeNumberForSessions flag, read from the org's 'Enforce Unique Blaze Number' setting — per the added comment, already returned on the session-details payload for the Issues tab/close gate, but now surfaced at the payment surface too so the counter can warn when a Transfer Receipt reference has already been used this session. This is a payment-reference / Transfer-receipt validation feature, not an order/session Closed-status change — worth a note in payment-method or session-details docs if any exist, not this entry. (2) IDistributedIdempotencyStore.cs (the in-memory/Redis-selectable order-update idempotency substrate) is deleted outright in this diff with no visible replacement in the shown hunks — internal plumbing only, not user-facing, and unrelated to this entry's claims; needs a check on OrderUpdateService's idempotency wiring before assuming behavior is preserved, but that's a codebase-health note, not a KB claim. Neither item changes when/whether an order reaches Closed status or a session moves through Open→PendingClose→Closed, so no edit is needed to this entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-09 — codebase (hubits-api-sales bb69312..7594413, tenancy pin / cache-invalidation seam / order micro-cache / SignalR routing envelope / dependency bump / feat(customers) paged statements). Not a contradiction of any claim this entry makes or any tracked sidecar claim: CloseRegister (:1592-1872), ApproveCloseRegister (:1965), the SalesRegisterSessionStatuses enum (:471-473), and the IsPendingClose/IsClosed DTO flags (:1414-1415) are absent from this diff and unchanged, and the Pay & Close atomic payment+ticket-close settle path is untouched. Everything in OrderPaymentController.cs (reserve-before-processing idempotency gate), OrdersController.cs/OrderUpdateService.cs (SignalR IOrderEventPublisher routing envelope), PaymentProcessingService.cs/TenderMath.cs (outlet-aware payment-reference allocation, tender-booking refactor), and CustomerOrderingApiNotificationService.cs (server-side customer-SMS webhook) restates items already logged as NEW_INFO on 2026-08-01/08-02/08-05/08-07 for this same base range — no new claim there. Two genuinely new-to-this-range hunks, both out of scope for this entry's Pay & Close / session-close claims: (1) SessionsController.OpenRegister now extracts the per-currency opening float into an openingAmount local and, per a new (truncated in this diff) comment, uses it to seed the session's Expected cash balance at open time ('with no sales and no withdrawals yet, "what should be in the drawer" IS the float') rather than presumably leaving Expected unset/zero until first activity — relevant to Cash Handling / Sales Sessions docs, not to order/session Closed status, and the comment is cut off mid-sentence in this diff so the exact field being set isn't fully visible; needs a live check on a fresh register open before documenting elsewhere. (2) OrderUpdateService.UpdateOrderAsync gains a new bomReversals list ('BOM stock to restore for removed/reduced prepared lines, applied after the diff, before save') feeding into the existing IsBomStockDeducted branch when a line item is voided during an order edit — an inventory-correctness fix for the order-edit path, not the payment/close path this entry documents; the hunk is truncated before showing the reversal being applied, so the exact restore mechanics aren't confirmed here. Neither item changes when/whether an order reaches Closed status or a session moves through Open→PendingClose→Closed, so no edit is needed to this entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-07 — codebase (foodops.counter.app f52b6dc..fe749d8, void-bin offline plumbing + SyncService test split + reason-capture snackbar mounted app-wide + discard buttons/read-only panel/live badge on dine-in). Not a contradiction of any claim this entry makes: the shown OrderDetailsPanel.tsx hunks leave the Pay & Close atomic payment+ticket-close path and the Closed/Open/Processing order-status set in the Sales Orders tab untouched, and this diff (frontend-only) doesn't touch SessionsController's CloseRegister/ApproveCloseRegister/status enum/DTO flags at all. The enableVoidBin flag → immediate handleVoidBinCancel() → voidOrderToBin() → optimistic 'cancelled' status mechanism shown here is the same void-bin cancel path already logged as NEW_INFO on 2026-08-04 against the same file (restated only because this diff's base range now runs to fe749d8, folding in a test-file split and two further void-bin commits — reason-capture snackbar mounted app-wide, and discard buttons/read-only panel/live badge on dine-in — whose own hunks aren't part of this cited-surface diff and so aren't evidenced here; still needs the live check already noted on 2026-08-04 for whether enableVoidBin is on for any org and whether a voided order still surfaces in the Sales Orders tab). One genuinely new hunk in this range: on a successful Credit/CreditAndClose settle, the submit handler now also calls invalidateCacheByPrefix('/customers') so the credit-payment picker reflects the customer's raised Outstanding balance instead of a stale pre-sale copy — a cache-freshness fix on the on-account settle path this entry's 'non-cash payment methods (card, on-account, loyalty)' line references in passing, but it doesn't change whether/when the order transitions to Closed, so no edit is needed to this entry. Out of scope for this entry; worth a note in customer-account/on-account-payment docs if any exist. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-07 — codebase (hubits-api-sales bb69312..7c2d16c, tenancy pin / cache-invalidation seam / order micro-cache / SignalR routing envelope / dependency bump / feat(customers) paged statements). Not a contradiction of any claim this entry makes or any tracked sidecar claim: CloseRegister (:1592-1872), ApproveCloseRegister (:1965), the SalesRegisterSessionStatuses enum (:471-473), and the IsPendingClose/IsClosed DTO flags (:1414-1415) are all absent from this diff and unchanged. The SessionsController.cs hunks shown are the constructor DI additions backing the close-summary endpoint and the PosDefaultViewMode/SoftEnforcePaymentReference DTO fields — both already logged as NEW_INFO on 2026-08-01/08-02/08-05 — plus one genuinely new item: GetMySessionsAndRegisters' OpenedTime now serializes as an explicit UTC ISO-8601 string (DateTime.SpecifyKind(s.OpenedTime, DateTimeKind.Utc).ToString("O")) instead of a naive yyyy-MM-dd HH:mm:ss; per the added code comment, the naive format had no zone marker so the browser's new Date(...) parsed it as local time and displayed the raw UTC clock (e.g. 03:02 instead of 08:02 in UTC+5), making a just-opened register look hours old. This is a real user-facing display-bug fix on the Sessions/Registers list, but it concerns the displayed session open time, not this entry's Open/PendingClose/Closed status semantics or the Pay & Close settle path — out of scope for this entry's claims, no edit needed. Worth a line in sales-sessions.md (or wherever the Registers list UI is documented) noting that pre-fix session-open timestamps could show several hours off for orgs outside UTC. Also out of scope, already covered structurally by the 2026-08-05 entry's 'SignalR routing envelope' commit-subject mention but now shown in full: OrdersController.cs (CreateOrder/UpdateOrderInternal/UpdateCustomer) and OrderUpdateService.cs move their SignalR broadcasts (OrderAdded/OrderUpdated/OrderStatusUpdated/KitchenOrderAdded/KitchenOrderUpdated) through a new IOrderEventPublisher.PublishAsync/Describe envelope carrying routing/scope metadata (OrderEventScope.Content) so terminals outside the relevant session skip a refetch — real-time delivery plumbing only; the same events fire with the same semantic payloads, no change to order status values or the Pay & Close flow. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-05 — codebase (hubits-api-sales bb69312..9351716, tenancy pin / cache-invalidation seam / order micro-cache / SignalR routing envelope / dependency bump / feat(customers) paged statements). Not a contradiction of any claim this entry makes or any tracked sidecar claim: the only SessionsController.cs hunks are (a) the close-summary endpoint already logged as NEW_INFO on 2026-08-01/08-02, restated here for completeness, and (b) two new org-level fields stamped onto SessionDto — PosDefaultViewMode (from the Outlets 'Default to Tile View' setting) and SoftEnforcePaymentReference (Outlets 'Soft Enforce Payment Reference Number', gates whether a Transfer payment requires a reference) — both display/config settings unrelated to session close or order status. CloseRegister, ApproveCloseRegister, the SalesRegisterSessionStatuses enum, and the IsPendingClose/IsClosed DTO flags are untouched. The reserve-before-processing payment idempotency guard (TryReserveOrReplayPaymentAsync) and the outlet-aware payment-reference allocation in PaymentProcessingService.cs are the same features already logged as NEW_INFO on 2026-08-01 and 2026-08-02 respectively, not new in this diff. One genuinely new item worth tracking, tangential to this entry's 'Edge case: payment captured but order still appears open' section: OrderPaymentController.cs adds an explicit order-cache invalidation seam (IOrderCacheInvalidator.InvalidateOrgAsync) because ExecuteUpdateAsync writes bypass the ChangeTracker-hooked OrderCacheInvalidationInterceptor — per the added code comment, without this call 'GET /orders/{id} keeps serving the pre-write order... until the entry ages out at the TTL.' This is plausibly the backend mechanism behind exactly the stale-order-view symptom this entry's edge-case section already tells cashiers to fix by refreshing the Counter page; it doesn't change any documented behavior (the advice to refresh still holds) but is worth a forward-reference once live-verified that stale-after-payment reads are now rarer. Also out of scope: a new CustomerOrderingApiNotificationService.cs moves the customer-SMS status webhook (order operation status, e.g. Accepted/Ready/Delivered) server-side for auth reasons — this is the restaurant-operations status pipeline, not the payment/Closed-order status this entry documents, so no cross-reference needed here. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-04 — codebase (foodops.counter.app f52b6dc..37ad39c, 'feat: void bin offline plumbing' / 'feat: void-to-bin engine + immediate cancel wiring + fix synced-void pending bug' / 'feat: void bin reason-capture snackbar'). Not a contradiction of any claim this entry makes or any tracked sidecar claim: none of the shown hunks touch SessionsController's CloseRegister, ApproveCloseRegister, the SalesRegisterSessionStatuses enum, or the IsPendingClose/IsClosed DTO flags, and the cash Pay & Close atomic payment+ticket-close path this entry documents is unmodified. NEW_INFO, out of scope for this entry's Pay & Close/session-close claims but adjacent to the passing 'cancel or complete all outstanding orders before running Close Register' advice in the Common questions section: OrderDetailsPanel.tsx's Cancel Order button now branches on a new per-session enableVoidBin flag (read via getCurrentSession() on mount). When enabled, handleCancelOrder no longer opens the existing confirm dialog (setShowCancelDialog) — it calls handleVoidBinCancel() immediately, which calls voidOrderToBin(...) and, on success, optimistically sets the order's status to a new value, 'cancelled' (onOptimisticStatusChange?.(order.id, 0, 'cancelled')), distinct from the Closed/Open/Processing status set this entry documents in the Sales Orders tab. Reason capture moves to a persistent, stacking snackbar rather than an inline dialog (per the other commit subjects), so the cancel UX changes materially when the flag is on. This is a cancel/void mechanism, not a payment or session-close mechanism, so no edit is needed to this entry's Pay & Close or session-status claims — but the order-cancellation doc should eventually note the new 'cancelled' status and void-bin flow, and this entry's brief 'cancel outstanding orders' line may need a cross-reference once that doc exists. Needs a live check to confirm whether enableVoidBin is actually enabled for any org yet (no admin/toggle UI accompanies this diff) and whether a voided order still surfaces in the Sales Orders tab or is filtered out entirely. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-02 — codebase (hubits-api-sales bb69312..1b4f871, tenancy pin / cache-invalidation seam / order micro-cache / SignalR routing envelope / feat(customers) statements / dependency bump). Not a contradiction of any claim this entry makes or any tracked sidecar claim: the only SessionsController.cs hunk is a pure insertion — two new endpoints, GET {sessionId}/close-summary and {sessionId}/close-summary/pdf, placed between GetSessionDetails and GetDisplayName, plus new DI for DaySummaryReportService/IHtmlToPdfService — CloseRegister (:1592-1872), ApproveCloseRegister (:1965), the SalesRegisterSessionStatuses enum (:471-473), and the IsPendingClose/IsClosed DTO flags (:1414-1415) are absent from this diff and unchanged. Those close-summary endpoints, and OrderPaymentController's move to a reserve-before-processing idempotency guard (TryReserveOrReplayPaymentAsync, claims the RequestId row before work starts, 2-minute in-flight lease before a retry may take over an abandoned reservation), were already logged as NEW_INFO on 2026-08-01 for this same base range (bb69312..01d4b26) — restated here only for completeness, not as a new claim. One genuinely new item in the 1b4f871 delta: PaymentProcessingService.cs's payment-reference allocation is now outlet-aware. ResolveOutletIdForRegisterSessionAsync + GetPaymentReferenceBlockAsync/GetPaymentReferenceAsync (new OutletHelper methods) draw from the register's own outlet-scoped Payment sequence (its configured prefix) when that outlet has UseUniqueReferenceNumbers enabled, falling back to the org-wide sequence otherwise — still one atomic block-allocation call, so the gap-free/single-lock-round-trip property is preserved. This sits on the same Pay & Close settle path this entry documents, but only changes which reference number a payment/receipt is stamped with, not whether or when an order or session transitions to Closed — no edit needed to this entry's claims. Out of scope for this entry; worth a note in payment/receipt-numbering docs if any exist, and needs a live multi-outlet check before documenting since no foodops.counter.app frontend changes accompany it in this diff. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-01 — codebase (hubits-api-sales bb69312..01d4b26, tenancy pin / cache-invalidation seam / SignalR routing envelope / feat(customers) statements). Not a contradiction of any claim this entry makes or any tracked sidecar claim: none of the shown hunks touch SessionsController's CloseRegister, ApproveCloseRegister, the SalesRegisterSessionStatuses enum, or the IsPendingClose/IsClosed DTO flags — the only SessionsController hunk is a pure insertion (two new close-summary endpoints) placed between GetSessionDetails and GetDisplayName, which will push this entry's pinned line numbers (:1592-1872, :1965, :471-473, :1414-1415) down at the next re-pin but does not change what those methods do or the three-state machine they implement. Two items are NEW_INFO, out of scope for this entry's existing claims: (1) SessionsController gains GET /{sessionId}/close-summary and GET /{sessionId}/close-summary/pdf — a session-level 'Session Summary' thermal-receipt/PDF built via DaySummaryReportService so its figures reconcile with the day-summary report; this is a session-close artifact (a receipt for the whole shift), distinct from the per-order payment receipt this entry already documents, and worth a line in the 'When does a session move from Open to Closed' walkthrough or in sales-sessions.md once live-verified — unclear from this diff whether the counter UI (register-sessions details page) exposes a button for it yet, since no foodops.counter.app changes accompany it. (2) OrderPaymentController's make-payment idempotency layer moves from write-after-processing to reserve-before-processing: TryReserveOrReplayPaymentAsync now claims the RequestId row before doing any work, treats an unfinished reservation younger than a 2-minute lease as 'in flight' (tells the retrying caller to back off) and only lets a request past the lease timeout take over an abandoned reservation. This hardens the exact Pay & Close settle path this entry documents against the counter's offline-queue retry sending make-payment twice — the same class of duplicate-write risk as the 2026-07-29 CounterLocalReference order-creation guard, but for payment submission specifically. Neither change alters when/whether a paid order's status moves to Closed or what Pay & Close looks like from the cashier's side, so no edit is needed to this entry's claims. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-29 — codebase (hubits-api-sales 9f561de..a26fdc7, 'feat(orders): implement duplicate-create guard for CounterLocalReference in OrdersController' / 'feat(caching): implement inventory stock invalidation publisher and integrate with payment processing'). Not a contradiction of any claim this entry makes or any tracked sidecar claim: SessionsController.cs (CloseRegister/ApproveCloseRegister/status enum/DTO flags) is untouched by this diff, and neither change alters when/whether an order's status moves to Closed or how Pay & Close settles. Two items are NEW_INFO but out of scope for this entry, worth tracking on adjacent docs instead: (1) OrdersController.CreateOrder now has a second, CounterLocalReference-keyed duplicate-create guard (30-min window) alongside the existing RequestId-based idempotency layer — it exists because a client retry that outlived the 120s RequestId reservation window created two real duplicate orders in prod (SO-12795/SO-12799, 2026-07-28); on a match it now returns the existing order instead of creating a new one. This is about order CREATION dedup, not order closing, so it belongs in a create-order/duplicate-order doc, not here. (2) PaymentProcessingService now publishes a cache-invalidation + SignalR 'MenuItemsUpdated' broadcast after both BOM/recipe stock deduction and plain stock-tracked invoice approval during the Pay & Close settle path, fixing stale stock levels on the admin app's menu items list and hubits-api-inventory's own cache. This is a backend freshness fix for a downstream surface (admin menu items), not a change to the order/session status semantics this entry documents — no edit needed here, but worth noting in stock/inventory-facing docs if any exist. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-28 — codebase (hubits-api-sales bf7ac10..8c00ab8, 'fix(counter-posting): paid POS sale always posts (force-allow-negative) + surface real invoice-creation failure reason'). Not a contradiction of any claim this entry makes or any tracked sidecar claim — all of it is in SessionsController.cs (CloseRegister/ApproveCloseRegister/status enum/DTO flags/SignalR label), none of which this diff touches; PaymentProcessingService.cs is the only file changed. NEW_INFO worth tracking for the 'Edge case: payment captured but order still appears open' section: on the Pay & Close settle path, CreateInvoiceFromOrder previously returned a bare null (surfaced only as the generic 'Failed to create invoice from order') on several failure modes — a stock-tracked item going negative, a duplicate invoice already existing for the order, or a downstream invoice-approval failure — any of which could leave a tendered payment without a completed invoice, i.e. a paid order that doesn't cleanly transition to Closed. The method now returns (invoiceId, failureReason) so the real cause propagates into the thrown exception, AND the stock-negative case specifically is no longer a failure at all: CreateInvoiceFromOrder is now called with forceAllowNegativeStock: true from the payment settle path (per the added comment, 'Payment has already been tendered on this path, so the sale must be recorded even if a stock-tracked item would go negative — force the allocation through rather than dead-lettering a completed sale. Stock going negative is reconciled by back-office'), and ChangeStatus's invoice-approval call also now takes forceAllowNegativeStock. Net effect: a paid POS order can no longer fail to close because of insufficient stock (that failure mode is eliminated on this path), and if invoicing does still fail (e.g. duplicate invoice) the cashier/agent should now see a specific reason rather than the generic message. Note the credit-invoice path (CreateCreditInvoice, unrelated to Pay & Close) still calls CreateInvoiceFromOrder without forceAllowNegativeStock, so it is unaffected. Worth a live check before adding to the edge-case list — confirm what error text actually surfaces client-side on a duplicate-invoice failure, and whether a negative-stock sale shows any indicator to the cashier. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-23 — codebase (foodops.counter.app dea0b59..61f13d1, 'feat(order-details): enhance change display with currency conversion in OrderDetailsCard'). Not a contradiction of any documented claim: this entry's 'computes change from the amount received' line in 'What Pay & Close does' is still accurate — change is still computed from coverage.changeBase, just rendered differently. NEW_INFO: the Order Details change line (previously a single {currencySymbol} {amount} span) now renders one amount per enabled currency, converting coverage.changeBase via a per-currency rate (rateFor(c.id)) and showing each as {code} {amount} — a direct UI continuation of the 2026-07-11 multi-currency tender work (ADR-0011/TenderMath.cs), now visible on the frontend for the first time in this diff history. Still backend-adjacent/display-only: no change to when/whether change is computed, no change to the Closed/Paid status semantics, and no change to the Pay & Close atomic settle path. Worth a live check on counter.foodops.io next verification pass to confirm how many currencies actually show (single-currency orgs presumably still see one line) before adding any UI-facing claim to the 'What Pay & Close does' section. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-22 — codebase (foodops.counter.app 69b3494..72c675f, 'feat(payment): implement BML Connect online payment integration with QR code support' / 'feat(payment): add online payment option with BML QR flow in OrderDetailsCard'). Not a contradiction of any documented claim: this entry's cash Pay & Close atomic payment+ticket-close path and the 'non-cash methods differ in presentation but the outcome is the same' claim are unaffected — the code comment confirms the order still settles once payment is fully captured, just asynchronously for this new method. NEW_INFO worth tracking: OrderDetailsPanel.tsx adds a BML Connect online/gateway payment option not covered by this entry's current non-cash list (card, on-account, loyalty). Unlike those, a gateway tender cannot settle synchronously on Pay/Pay & Close — when the selected split includes a tender whose payment method has requiresGateway, the settle handler now short-circuits (setBmlSettle(...), setProcessingAction(null); return;) and hands off to a new BMLQRCodeModal instead of calling makePayment directly; the diner scans a QR and pays in their BML app, and per the code comment 'the server settles it atomically on confirm (ADR-0013)'. A new 'Online gateway providers' section appears in the payment split UI when a gateway tender is present. This is a materially different settlement timing from the synchronous card/on-account/loyalty paths — worth a dedicated edge-case bullet or Q&A once live-verified on counter.foodops.io, specifically to confirm what the order's Status column shows (likely still Open, not Closed) while the BML QR modal is pending confirmation. Separately and out of scope for this entry: the same diff gates Cancel Order behind the SalesOrders.Delete permission (userPermissions?.canDeleteSalesOrders), which belongs to the order-cancellation doc, not this one. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-22 — codebase (hubits-api-sales 86e7d20..407e704, 'feat: enforce SalesOrders.Delete permission on order cancel endpoint' / 'feat(bml): implement BML Connect online payment integration and merchant account management' / 'feat(bml): update BML integration and configuration for online payments'). Not a contradiction of any documented claim in this entry — the cited SessionsController.cs CloseRegister/ApproveCloseRegister/status-enum lines and the Pay & Close atomic payment+ticket-close path are untouched by this diff; only OrdersController.cs and OrderPaymentController.cs (both in this entry's source_repos) changed. Two items worth tracking for adjacent docs, not yet actionable here: (1) CancelOrder now enforces the SalesOrders.Delete role server-side (org-scoped UserRoles lookup, not the 3-min-cached User.IsInRole) and returns HTTP 403 'You do not have permission to cancel orders' if the caller lacks it — relevant to this entry's Q&A advice to 'cancel or complete all outstanding orders before running Close Register,' since a cashier without that role will now hit a permission error attempting that cleanup rather than a silent success; (2) GetPaymentMethods now returns a RequiresGateway flag per payment method, added per the inline comment so the counter can distinguish an online-gateway tender (BML Connect) from cash/card and open a QR flow instead of settling directly (ADR-0013) — this is backend-only in this diff (no foodops.counter.app changes accompany it), so it's unclear whether/when a BML tender is actually exposed on the payment modal; needs a live check before adding it to the non-cash payment-method list in the 'What Pay & Close does' section, mirroring the 2026-07-11 multi-currency precedent. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-19 — codebase (foodops.counter.app 054a5b3..43c6fad, 'feat(bill-split): offline-first Equal split on the counter (Part 1)' / 'feat(counter): bill split by item' / cross-device sync commits). Not a contradiction of any documented claim: CloseRegister/ApproveCloseRegister and the session three-state machine are untouched by this diff, and the single-tender Pay & Close atomic payment+ticket-close path this entry describes is unmodified. NEW_INFO worth tracking: OrderDetailsPanel.tsx adds a distinct 'Bill Split' feature (Split icon, BillSplitModal/BillSplitSettle, feature-flagged via FEATURE_BILL_SPLITTING) letting a cashier divide one order into multiple parts (Equal or by-item), each paid separately and offline-first, gated by canSplitBill (flag on, order not locked/credit-sale/already-paid/completed/already-split, grossTotal>0). This is a different mechanism from the 'split payment' (multi-tender, e.g. part-cash/part-card on one invoice) edge case already documented above — Bill Split creates multiple independent parts/invoices per order, syncs across devices, and can be server-rejected/rolled back (bill-split-payment-rejected / bill-split-create-rejected events revert local optimistic UI state). Needs a live check on counter.foodops.io to confirm the feature flag is actually enabled and to capture how a fully-settled split order resolves in the Sales Orders tab's Status column, before adding any UI-facing claims here or spinning up a dedicated entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-11 — codebase (hubits-api-sales 5ff31ee..a7d5093, 'feat(payment): implement multi-currency support and tender application logic', ADR-0011). Not a contradiction of any documented claim: the SessionsController diff only touches GetSessionDetails' Expected-Cash read (removing a double-subtraction of cash withdrawals), nowhere near the cited CloseRegister (:1592-1872) / ApproveCloseRegister (:1965) code this entry's session-status claims rest on — the three-state machine and Pay & Close atomic-payment-plus-ticket-close behavior are unaffected. Separately, PaymentProcessingService.cs now supports per-tender currencies: non-cash tenders are applied before cash, each tender converts to base-currency via a configured fixed rate (TenderMath.cs, new pure helper), and change is still computed and dispensed in base currency for cash overpayment (0 for non-cash) — consistent with, not contradicting, this entry's 'computes change from the amount received' claim, just generalized beyond single-currency. Worth a mention in payment-method docs once confirmed live: this diff is backend-only (hubits-api-sales), no foodops.counter.app frontend changes accompany it, so it's unclear whether/when multi-currency tendering is actually exposed on the cash/payment modal UI — needs a live check before adding UI-facing claims. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-06 — codebase (foodops.counter.app 047ad31..0545649, commit 'feat(payment): confirm-and-charge for the stale-total guard'). Not a contradiction of documented Pay & Close behavior, but a new edge-case UI worth tracking: if the order's outstanding total changes on another device between opening the payment modal and tapping Pay & Close, the app previously showed a toast ('Order total changed') that silently re-filled a lone untouched tender and forced the cashier to press Pay again. It now instead surfaces an explicit confirm-and-charge dialog with the fresh amount, letting the cashier settle in one tap ('Charge Y') when there is a single untouched payment row (isLoneUntouchedTender); split/edited tenders still fall back to review-only. This only affects the stale-total edge case, not the normal single-device Pay & Close submission described in this entry — worth a short mention in the payment/split-payment docs (or an added edge-case bullet here) but does not require correcting any existing claim. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-06-29 — codebase (July-10 launch re-verify; hubits-api-sales@19190b9 main, foodops.counter.app@32669a9 main). UPHELD: three-state machine Open → PendingClose → Closed — CloseRegister (SessionsController.cs:1592-1872) sets SalesRegisterSessionStatuses.PendingClose on every close (:1872), the variance only changes the SignalR label (:1885/:1924), and the separate [HttpPost("approve-close")] ApproveCloseRegister (:1965) is what reaches Closed; IsPendingClose/IsClosed DTO flags at :1414-1415; status ids at :471-473. Pay & Close order-level atomic payment + ticket-close re-confirmed in OrderDetailsPanel.tsx (selectedPaymentMethods settle → make-payment). CORRECTED: drifted SessionsController line citations re-pinned to current main; the legacy app.foodops Session History (/salessessions) Approve-Close surface is flagged retired at launch (counter app /register-sessions is now the only deferred-approval surface); source_repos re-pointed off the removed PaymentSettlementModal.tsx to OrderDetailsPanel.tsx + register-sessions. Frontmatter SHAs + last_verified bumped.

  • 2026-06-29 — LIVE (T6 doc-as-test) · drove the documented walkthrough on counter.foodops.io (KB Test Restaurant, salesmade-verification agent). NEEDS-FIX → applied: the order Status in the Sales Orders tab is Closed, not "Paid" (the prior label was legacy app.foodops); "Paid" survives only as a payment-section indicator ("This order has been fully paid."). Corrected the status label throughout (intro, the "what Closed means" section, Order Details badges, Key concept, 2 Q&A) and clarified the order-"Closed" vs session-"Closed" collision. Also: Close Register is a button (not a text link). Screenshots in /tmp/claude-1001/kb-doctest/ (close-paid-step04 Sales Orders "Closed"; step05 single Closed badge + Paid indicator; step08 Close Register button) → pending T4 media refresh. Order/session three-state machine (code) unchanged.

Note: Code-verified 2026-06-04 · hubits-api-sales@5274891 (main) Verified_by: codebase · Last verified: 2026-06-04 REAL CHANGE: session close is now a three-state machine Open → PendingClose → Closed. SessionsController.CloseRegister (:1745-1753) sets the status to PendingClose on every cashier close (the old "no variance → straight to Closed" branch was removed); a separate [HttpPost("approve-close")] endpoint (:1846-1916) is what transitions PendingClose → Closed, stamping ApprovalClosedTime/ApprovalClosedByUserId. The SalesRegisterSessionStatuses enum exposes Open/PendingClose/Closed (:431-433, 469-471) and SessionDto/SessionDetailsDto carry IsOpen/IsPendingClose/IsClosed. Corrected the "Open → Closed" section, the session-status Key concept, and added two Q&A. Pay & Close order-level behaviour (atomic payment + ticket close) re-verified unchanged — order/session split still correct. Order status badges (Accepted → Paid → Closed) unchanged (live-verified 2026-05-04, screenshot 117). LIVE-VERIFIED 2026-06-11: Approve Close surfaces on the counter app's Register Sessions → session details page ("Approve Close" + "Adjustment" buttons, screenshot 207) and on the legacy Session History details page ("Approve Close Register" + "Adjustment" links, screenshot 186); the at-close Balance Difference Warning dialog's "Close Anyway" button approves immediately (screenshot 184). Pending Close status pill confirmed on both session lists (screenshots 185, 206).

Warning: Unverified — status: draft Verified_by: claude-code (internal-docs read, 2026-05-03) · Code access: not available (private repos) Claims derive from internal-docs/foodops/counter/counter-order.md (verified 2026-04): Pay & Close is an atomic payment + ticket-close confirmed; receipt confirmation modal confirmed; new blank ticket opens on same session confirmed. internal-docs/foodops/counter/session-details.md (verified 2026-04-23): Sales Orders tab with Status column confirmed. Order-lifecycle vs session-lifecycle separation is an inference from the two documents read together; no contradiction found. Order status enum (Paid, Open, possible Cancelled) marked <!-- UI-TODO --> pending live re-capture.

Go beyond POS.
Choose the total solution.

Focus on creating the best experience for your guests while we handle the rest.