Plain-English summary
Session reconciliation is the umbrella term for everything that has to add up at the end of a register shift — every order booked, every payment received, every cash event in the drawer, and the variance between expected and counted at close. The till reconciliation (drawer count vs Expected) is the headline component, but session reconciliation is broader: it is the audit promise that the session's full record is internally consistent and externally defensible. The output is a closed session with a Z-report, a known variance, and a record of who opened and closed the till.
When you'd use this
- A manager reviewing the day's sessions wants to understand what "reconciled" means as a holistic claim, not just one number.
- A new employee asks "what does it mean to close a register cleanly?" and the answer needs to span more than just the drawer count.
- An auditor is verifying that a session's orders, payments, drawer movements, and reports are all mutually consistent.
- Drift detection — comparing what the Z-report printed against what the receipts and order records show separately. Session reconciliation is the model that ties them together.
- Building a manager dashboard that surfaces "session reconciled" vs "session has anomalies" — needs a clear definition of what reconciled means.
What session reconciliation covers
Five layers of things-that-must-add-up. Each independently is its own concept; together they constitute a fully reconciled session.
Layer 1 — Orders ↔ Tickets
Every order taken in the session is recorded in the Sales Orders tab of Session Details. Order count, subtotals, discounts, taxes — all attached to the session. A reconciled session has orders whose statuses are terminal (Paid, Cancelled, Refunded) — no in-progress tickets dangling at close. See Close Paid Order for the order-level lifecycle.
What can go wrong: a ticket left in a non-terminal status when the cashier tries to close. Unlike the cash variance, this does not slip through — the API blocks the close ("Please close all orders before closing register") until every order is Completed or Cancelled [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1606-1621]. The cashier has to resolve the ticket first.
Layer 2 — Payments ↔ Receipts
Every payment captured produces a receipt, surfaced in the Receipts tab. Receipts sum per payment method (Cash, Card, On Account, Loyalty, etc.) and feed the per-method totals on the Z-report. A reconciled session has every payment matched to a receipt, with receipt totals matching order totals after discounts/taxes/refunds.
What can go wrong: a payment captured but the receipt write failed (rare, network issue), producing a receipt-less payment that does not aggregate into the per-method totals. Surfaces as a discrepancy between order amounts and receipt amounts.
Layer 3 — Cash events ↔ Expected balance
The cash-side of the session has its own ledger:
- Opening Balance (counted in at open, by denomination)
- Cash Sales (sum of successful cash payments) [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1700-1729]
- Cash Refunds (cash returned to customers). Note: the dedicated reconciliation views (
show-cash-reconciliation,view-reconciliation-details) compute Expected asopening + cash-pays − cash-refunds, separating Pay-type and Refund-type payments [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:ShowCashReconciliation @3676-3693]. The close-register path, however, does NOT have a separate refund term — its cash query selects all successful cash payments without distinguishing Pay vs Refund [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1700-1705]. - Cash Withdrawals (manager pulls recorded in the Cash Withdrawal tab, subtracted from Expected) [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1731-1745]
- Delivery Cash Acknowledged (rider returns ticked off in Delivery Cash) — tracked via
acknowledge-cash-handovers[code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:AcknowledgeCashHandovers @3406-3508]
Together these produce the Expected figure on the Overview tab. See Cash Handling for how each event is logged.
What can go wrong: an unrecorded withdrawal (cashier or manager pulls cash without using the form) or an unacknowledged delivery deposit. Either inflates or deflates Expected, producing a phantom variance at close.
Layer 4 — Drawer count ↔ Expected (the variance)
At close, the cashier counts the drawer per denomination. The total Counted is compared to Expected (specifically Counted − (Opening + Expected)), and any gap larger than a 0.01 rounding tolerance is flagged as a variance [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1776-1794]; the gap is the variance. The Comments narrative explains it. This is the till reconciliation layer — the most-watched piece of session reconciliation but only one of five.
What can go wrong: variance with a vague or empty Comments narrative — manager triage cannot proceed, escalation often follows.
Layer 5 — Audit trail ↔ Cashier-of-record
Throughout the session, the till lock may transfer between cashiers via session handover — tapping Join on an open session sets that session's LockedByUserId to the incoming cashier [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:JoinSession @517-560]. Important: the session record permanently retains only who opened it (OpenedByUserName) and who closed it (ClosedByUserName); the session-details payload surfaces no per-handover history list [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:GetSessionDetails (SessionDetailsDto) @1343-1412], and a Join writes no audit-log row (only an application log line) — see the verified Session Handover entry. So the "chain of custody" is real only at the open/close endpoints plus the current lock holder; it is NOT a minute-by-minute, fully-auditable record of every cashier who held the till. (The "Handover" tab in Session Details is titled "Cash Reconciliation & Handover" and records mid-shift cash counts, not till-ownership transfers.)
What can go wrong: a handover performed without the incoming cashier tapping Join, so the session's LockedByUserId still names the previous cashier while someone else is actually working the till. Because no per-handover record is written, this is discoverable only when something specific goes wrong (variance spike, customer complaint at a specific time) — and even then the system only shows the open/close cashiers and the last lock holder, not the full sequence.
When is a session "reconciled"?
A session is operationally reconciled when:
- Every order is in a terminal status.
- Every payment has a corresponding receipt.
- Every cash event was recorded as it happened.
- The drawer count was honestly entered with a Comments narrative.
- The recorded cashiers (who opened, who closed, current lock holder) reflect who was actually on the till — noting the system keeps no full per-handover log.
- The session is closed (status = Closed).
This is stricter than "the session is closed." Some layers are enforced at close (a non-terminal order or held sale blocks the close outright [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1591-1621]), but others are not — a session can still close with a vague Comments narrative, a missed delivery-cash acknowledgement, or an unexplained cash variance. Those are technically closed but not fully reconciled. The distinction matters for manager dashboards and audit reports.
How session reconciliation differs from till reconciliation
Till reconciliation is Layer 4 only — the drawer count vs Expected calculation. It is the noun cashiers use ("close-out the till").
Session reconciliation is all five layers — the full close-shift accounting. It is the noun managers and auditors use when reviewing whether a session is internally consistent.
Many people use the terms interchangeably; the distinction is most useful when a session reconciles at the till (variance zero) but fails on another layer (e.g., a customer-paid order with no receipt). Saying "the till reconciled but the session did not" is shorter than spelling out the gap.
What surfaces session reconciliation in the UI
- Overview tab in Session Details — per-payment-method Opening / Counted rows are always shown; the Expected and Difference columns are now conditionally rendered ("blind cash counting") based on a
canSeeExpectedForSession(userPermissions, session?.isClosed)permission check, so a cashier without that permission — or counting before the session is closed — does not see Expected/Difference on this tab [code: foodops.counter.app/src/pages/RegisterSessionDetailsPage.tsx]. - Sales Orders tab — every order with its status. Open orders at close are visible here.
- Receipts tab — every payment receipt; each carries a per-payment Reconciled flag, set via the update-payment-reconciliation endpoint [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:UpdatePaymentReconciliation @4446-4519].
- Cash Withdrawal tab — every recorded mid-shift pull.
- Delivery Cash tab — rider returns, acknowledged or pending [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:AcknowledgeCashHandovers @3406-3508].
- Handover tab (titled "Cash Reconciliation & Handover") — records mid-shift cash counts, not till-ownership transfers; see the verified Session Handover entry. It is NOT a cashier-of-record chain-of-custody log (the session-details payload
SessionDetailsDtocarries no per-handover history) [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:GetSessionDetails (SessionDetailsDto) @1343-1412]. - Z-report — printed at close, summarises Layers 2–5 in one document.
- Sales Sessions list (
/salessessions) — historical view of every session, status, duration, closing balance.
A fully reconciled session looks consistent across all of these. An anomaly typically surfaces in one tab and points at the underlying layer.
Closing a session that does not fully reconcile
Foodops does block close on some reconciliation failures, but not on a cash variance. Before the count is even processed, close-register hard-rejects (HTTP 400, nothing persisted) if the session still has on-hold sales ("Please unhold all sales before closing register"), open sales orders — any order whose status is neither Completed nor Cancelled — ("Please close all orders before closing register"), unresolved transfer-reference ("blaze") issues when the org enables EnforceUniqueBlazeNumber ("Resolve transfer reference (blaze) issues before closing the register."), or sales still posting / dead-lettered in the financial-posting outbox [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1636-1700 (on-hold L1636, open-orders L1653, blaze gate L1657-1672, unposted/dead-letter L1674-1700)]. So the Layer-1 "open ticket at close" scenario is actually prevented at the API, not merely flagged.
What is not blocked is the Layer-4 cash variance. Once those gates pass, the close is a two-step action: the cashier submits the count, the session lands in Pending Close rather than straight to Closed regardless of variance [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1817-1821], and a supervisor then approves it ("Close Anyway") [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:ApproveCloseRegister @1914-1979], adjusts the count [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:AdjustRegister @2152-2320], or cancels the close [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CancelCloseRegister @2060-2114]. The cashier is never stuck on the variance — the count is captured the moment they submit — but the variance now gets an explicit supervisor sign-off before the session record is finalised. This formalises the manager triage that used to be purely downstream.
The cashier's responsibility is honesty in the Comments and accuracy in the count. The manager's responsibility is the cross-layer review — and now an explicit approval/adjust/cancel decision on the Pending Close. See Session Lifecycle for the state machine.
Key concepts
- Session reconciliation (broad) — the complete close-shift accounting across orders, payments, cash events, drawer count, and audit trail.
- Till reconciliation (narrow) — drawer count vs Expected only. Layer 4 above.
- Reconciled session — one where all five layers are internally consistent and the session is closed.
- Anomaly — a layer that does not reconcile (missing receipt, unrecorded withdrawal, vague Comments, missing handover record). Some are blocked at close (open ticket, held sale, unposted sale, and — when
EnforceUniqueBlazeNumberis on — blank/duplicate transfer references [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1636-1700]); the cash variance and the soft layers are not. - Chain of custody — who is accountable for the till. In practice the system records only the opening cashier, the closing cashier, and the current lock holder (
LockedByUserId), not a full per-handover progression. Layer 5.
- Expected formula — the cash-side calculation feeding Layer 4. In the
close-registerpath,ExpectedAmount(base currency) = (sum of successful cash payments for the currency) − (recorded, non-deleted cash withdrawals), and the variance isCounted − (Opening + ExpectedAmount)[code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1707-1745, 1776]. The close path applies NO explicit refund-subtraction term (its cash query does not separate Pay- from Refund-type payments) — that explicit− refundsterm exists only in the separateshow-cash-reconciliation/view-reconciliation-detailsviews [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:ShowCashReconciliation @3683-3693]. - Z-report — the printed summary that surfaces Layers 2–5 to managers.
Common questions
Q: Is "session reconciled" the same as "session closed"? A: No — closed is the system status; reconciled is a stronger claim about internal consistency across orders, payments, drawer, and audit trail. A session can be closed but not fully reconciled (e.g., open ticket at close).
Q: Where do I see the reconciliation status of a session? A: Foodops does not currently expose a single "reconciled / not reconciled" flag. The status is derived by reviewing each tab: Sales Orders for open tickets, Receipts for unmatched payments, the variance figure for till reconciliation. Manager dashboards in some deployments aggregate these into a derived flag.
Q: Will the system stop me from closing if a layer doesn't reconcile?
A: It depends on the layer. The system does stop the close if there are on-hold sales, open (non-terminal) sales orders, unresolved transfer-reference "blaze" issues (when EnforceUniqueBlazeNumber is on), or sales still posting/dead-lettered — close-register returns an error and persists nothing until those are resolved [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1636-1700]. It does not stop the close on a cash variance: once those gates pass, the count is recorded and the session lands in Pending Close, where a supervisor must approve it before it becomes terminal [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1817-1821; ApproveCloseRegister @1914-1979]. So a dangling open ticket is prevented up front; the variance is the thing that flows to supervisor sign-off rather than being blocked outright.
Q: How do receipts marked "Reconciled" relate to session reconciliation?
A: The Reconciled flag is a per-payment accounting mark, toggled by a dedicated endpoint that flips payment.IsReconciled and writes an audit log entry [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:UpdatePaymentReconciliation @4446-4519]. It is typically used to indicate the receipt has been matched against a bank deposit or card-reader settlement. It is one specific signal within the broader session reconciliation; not the same as the session as a whole being reconciled.
Q: Does the system track which sessions are NOT reconciled? A: Indirectly — anomalies surface in their respective tabs (open orders in Sales Orders, unreconciled receipts in Receipts, non-zero variance on the Z-report). There is no single dashboard query for "all unreconciled sessions" out of the box, but exception reports can be built from these signals.
Q: How long after close should reconciliation be reviewed? A: As soon as practical, typically same day or next morning. The longer the delay, the harder it is to investigate (cashier may not remember details, physical evidence may be gone). Daily review is the common pattern.
Q: Can session reconciliation be partially automated? A: The cash-side ledger (Layer 3) is fully automated — every recorded event updates Expected. The order-vs-payment cross-check (Layers 1+2) is also automatic at the data level. The cashier still has to enter the drawer count (Layer 4) and the handover record (Layer 5) by hand. So 60-70% of the reconciliation is automated; the remainder is the cashier's job.
Q: What is the relationship between session reconciliation and the Z-report? A: The Z-report is the artefact that summarises session reconciliation across Layers 2 through 5. It is the document a manager reviews to verify reconciliation; the underlying data is in the tabs.
Edge cases and known issues
Open tickets block the close
- Symptom: Close Register is rejected with "Please close all orders before closing register" (or "Please unhold all sales before closing register" for held sales).
- Cause: one or more orders in the session are still in a non-terminal status (not Completed, not Cancelled), or there are on-hold sales. The API refuses to close the register while any of these exist [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister @1591-1621].
- Workaround: walk through the Sales Orders tab and resolve (complete or cancel) each open ticket, and unhold any held sales, then retry the close. Because the close is blocked, a closed session never carries a dangling open order from this path.
Receipts tab shows payments not in the Sales Orders tab
- Symptom: the Receipts list contains a transaction with no corresponding order.
- Cause: data anomaly — usually indicates a payment-write that succeeded while the order-write failed, or a duplicated submit.
- Workaround: escalate. The session is technically closed but Layer 1 + Layer 2 do not reconcile.
Cash variance large and unexplained on a session that "looks fine" everywhere else
- Symptom: orders all paid, receipts all matched, withdrawals all recorded, but the drawer is short by a meaningful amount.
- Cause: typically an unrecorded cash event (off-book pull, tip-jar comingling) or a counting error at open or close.
- Workaround: investigate the Comments narrative; review Handover history; if no explanation surfaces, escalate to management. The session reconciles on every layer except 4, which is enough to require formal review.
Related
Sales Sessions — the entity reconciliation operates on
Sales Registers — the register that anchors the session
Session Lifecycle — reconciliation is the work done during the Closing transition
Till Reconciliation — Layer 4 in detail
Cash Handling — Layer 3 in detail
Cash Variance — the headline number from Layer 4
Z-Report — the close artefact summarising Layers 2-5
Session Handover — Layer 5 audit log
Close Paid Order — order vs session lifecycle separation, relevant to Layer 1
Close Register form (internal-docs) — the screen where reconciliation submits
2026-08-14 — codebase (foodops-admin-app 4a70096..8f9feb5). NEW_INFO, not a contradiction: RegisterSessionDetailsPage.tsx (admin-app session-details surface, one of this entry's cited source files) gains two new user-facing capabilities. (1) A 'stale session' notice — new SessionStaleNotice component plus a new src/utils/sessionAge.ts helper set (isStale, daysSince, msSince, formatSpan, ranFor) — surfaces when a session has been open unusually long, alongside a new SessionActionChecklist and a facts = summarise(detail) memo feeding the header/notice/checklist. This is relevant to this entry's 'manager dashboard... surfaces session reconciled vs session has anomalies' use case and the anomaly-detection framing throughout the doc, but the exact staleness threshold and checklist contents aren't visible in the supplied hunks (summarise()/isStale() bodies not included) so shouldn't be asserted precisely yet. (2) Client-side search/filter added across the Orders/Invoices/Payments list tabs via a new tabSearch state and matches() helper (case-insensitive substring match over reference/customerName/createdByUser/status-type fields), reset on tab switch so a search typed on one tab doesn't silently narrow another — a UI convenience for finding a specific order/invoice/payment when auditing a session, relevant to the 'What surfaces session reconciliation in the UI' section's Sales Orders/Receipts-equivalent tabs. Also cosmetic: the old ad hoc HeroStat card component was deleted in favor of a new SessionOverviewPanels component, and the Overview-tab code comment changed from 'renders the Day Summary report' to 'renders the Day Summary figures' alongside removal of the DaySummaryReportBody import — wording/composition change only, not a claim this entry makes about admin-app's Overview tab. No SessionsController.cs (backend) changes in this diff — the five-layer model, close gates (on-hold/open-order/blaze/unposted), Expected formula, PendingClose two-step, and IsReconciled flag this entry documents are all unaffected. Worth confirming isStale()'s exact threshold and SessionActionChecklist's contents against the full component source before folding a 'stale session' bullet into the body. (auto-applied by the truth pipeline; adversarially gated)
2026-08-12 — codebase (foodops.counter.app a00083c..d35a1ba). This diff's RegisterSessionDetailsPage.tsx hunks mostly restate what the 2026-08-10 entry already logged (the invoice-reference-as-link-to-admin-app feature via adminAppUrl, and the formatDurationSince parseServerDate timezone fix) — no new action needed on those. One genuinely new item beyond that prior note: the session's invoice table now adds a 'Customer' column between Reference and Invoice Date, rendering invoice.customerName or falling back to 'Walk-in Customer' when absent. This is a small UI addition to the same invoices list flagged on 2026-08-10 as relevant to 'What surfaces session reconciliation in the UI' (Layer 1/Layer 2 cross-checking) but not yet named as a distinct 'Invoices' list in the entry body. Worth folding in alongside the invoice-link item if/when that section gets a rewrite — a manager can now see which customer an invoice belongs to directly from the session-details invoices table, useful for tracing a Layer 2 (payment/receipt) discrepancy back to a customer. No backend, Expected-formula, close-gate, or five-layer change in this diff; the other commit subjects in this batch (poisoned-customer sync fixes, dine-in Take Out/Delivery button hide) touch no hunks intersecting this entry's cited surface. (auto-applied by the truth pipeline; adversarially gated)
2026-08-10 — codebase (foodops.counter.app a00083c..7c23e6d). NEW_INFO, not a contradiction: RegisterSessionDetailsPage's invoice list previously rendered
invoice.referenceas plain text; it now renders the reference as a link to the admin app's invoice detail page (adminAppUrl('/sales/invoices/${invoice.transactionId}'), new tab) whenever the invoice carries atransactionId, falling back to plain text otherwise. This gives a manager/auditor reviewing a session's invoices a direct one-click path into foodops-admin-app's per-invoice record — relevant context for the 'What surfaces session reconciliation in the UI' section (session-details surface) and the Layer 1/Layer 2 cross-checking workflow, though the entry does not currently name an 'Invoices' list distinct from the Sales Orders tab, so this should be confirmed as in-scope before adding a bullet. Also in this diff but unrelated to reconciliation: a bug fix changingformatDurationSinceto parse the session-opened timestamp viaparseServerDateinstead ofnew Date, correcting a terminal-timezone-offset error in the running-session duration badge — cosmetic only, no change to Expected/variance/close-gate logic. No SessionsController.cs or backend changes in this diff; none of the entry's cited Layer 1-5, Expected-formula, or close-gate claims are touched. No body edit needed; flagging in case the invoice-link feature is folded into a future 'What surfaces session reconciliation in the UI' rewrite. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-07-31 — codebase (foodops.counter.app 5468530..5ac2982). NEW FEATURE: session close-summary printing/PDF export. RegisterSessionDetailsPage.tsx adds 'Print Receipt' (thermal, via a configured print broker) and 'Save PDF' actions that fetch a SessionCloseSummary (sessionApiService.getCloseSummary/getCloseSummaryPdf), described in a code comment as sourced 'from the same aggregation as the Day Summary report, so the printed slip can never show different amounts than the Day Summary.' Both actions render only when canViewExpected is true — the same flag driving the 2026-07-30 'blind cash counting' Expected/Difference gate — with a comment (not independently verified in this diff) stating the rule is 'closed → everyone; open/pending → only with the view expected right.' This adds a new close-time artifact alongside the Z-report, relevant to the 'What surfaces session reconciliation in the UI' section and Layer 4, but does not change any backend Expected/variance computation this entry cites (no SessionsController.cs changes in this diff). Worth documenting once getCloseSummary/getCloseSummaryPdf and the canViewExpected permission rule are confirmed against the API and useUserPermissions.ts source. (auto-applied by the truth pipeline; adversarially gated)
2026-07-30 — codebase (foodops.counter.app c4cf5dc..5468530). NEW FEATURE: 'blind cash counting'. RegisterSessionDetailsPage.tsx's Overview tab now conditionally hides the Expected and Difference columns via a new canSeeExpectedForSession(userPermissions, session?.isClosed) check (imported from a new src/hooks/useUserPermissions module not included in the supplied diff hunks). Per the commit subjects ('hide expected implementation'; 'blind cash counting — hide expected/variance until register session close'; 'blind-close variance summary modal — neutral-black figures, per-currency cards'), the product intent is to stop the cashier seeing the Expected total (and hence the variance) while counting the drawer, revealing it only after close via a dedicated blind-close variance summary modal. This changes the presentation of this entry's Layer 4 (Drawer count ↔ Expected) and the 'What surfaces session reconciliation in the UI' Overview-tab bullet, but does NOT change the backend computation this entry cites (ExpectedAmount, the 0.01 variance tolerance, Counted − (Opening + Expected) in SessionsController.cs) — only the counter-app presentation layer changed in this diff. The exact permission rule driving canSeeExpectedForSession (which roles see Expected, and whether visibility unlocks purely on isClosed or also depends on role pre-close) was not in the supplied hunks and should be confirmed against useUserPermissions.ts before writing a fuller Layer-4/UI-surfacing rewrite. (auto-applied by the truth pipeline; adversarially gated)
2026-07-19 — codebase (foodops.counter.app b43110b..054a5b3, counter.foodops.io). Frontend-only bug fix, not a backend contract change: RegisterSessionDetailsPage's session-details read is served from a cache-first store with a 5-min TTL and no background revalidation, so any read following a write (ApproveCloseRegister, CancelCloseRegister, recompute, fix-duplicate-counts) previously could repaint the pre-write snapshot. Concretely, approving a Pending Close (the supervisor 'Close Anyway' step documented in this entry's 'Closing a session that does not fully reconcile' section) is usually done by a supervisor acting on someone else's register, so a guard that only purged the cache for the acting user's own session left the stale PendingClose snapshot in place — the UI kept showing 'Pending Close' until the cache aged out or another window's fetch overwrote the shared IndexedDB entry, even though the backend ApproveCloseRegister call had already succeeded. The fix adds a purgeSessionCaches() call (clearing /sessions/my, /sessions/{id}/details, and the /sessions/history prefix) unconditionally after approve/cancel/recompute/fix-duplicates, plus a forceRefresh flag on the details fetch. This does not alter the CloseRegister/ApproveCloseRegister/CancelCloseRegister/AdjustRegister API behavior, the five-layer model, the close gates, or the variance formula this entry documents — it only means the UI now reflects a completed approve/cancel/recompute immediately instead of intermittently lagging behind the true session state. No entry edit needed; noting in case a future 'why does this session still say Pending Close after I approved it' report references a pre-054a5b3 build. (auto-applied by the truth pipeline; adversarially gated)
2026-07-06 — codebase (hubits-api-sales 6f242ad..e31810a, main). NEW_INFO, not a contradiction: OpenRegister, CloseRegister, CancelCloseRegister, and AdjustRegister now each write an operation-log entry via session.TouchCreate()/Touch() ('Register opened.', 'Session closed. ...', 'Session close cancelled. ...', 'Session adjusted. ...'), and a new GET {sessionId}/history endpoint (GetSessionAuditHistory) surfaces the full per-session operation-log timeline — action label, OccurredAt, Actor (user full name/username, else 'System'), Remarks, and parsed before/after field diffs — newest first, described as mirroring the catalog/finance History tabs. This formalises audit-trail coverage for the open/close/cancel/adjust lifecycle actions referenced in Layer 5 and the 'Chain of custody' key concept. It does NOT touch JoinSession in this diff — Join still only sets LockedByUserId with no accompanying operation-log write here, so the entry's core finding (till-lock handovers via Join are not captured in a full per-handover audit log, only the current lock holder is retained) remains accurate and unaffected. Worth a forward-looking note in Layer 5 / Session Handover / Z-report cross-references once a UI surfaces this new history endpoint, since it strengthens (but does not yet close) the audit-trail gap the entry describes. (auto-applied by the truth pipeline; adversarially gated)
2026-07-04 — codebase (hubits-api-sales e4f8b14..aa4a8d5, GetSessionDetails/GetSessionHistory/GetSessionHistoryStats in SessionsController.cs, not the CloseRegister/ShowCashReconciliation ranges this entry cites). Two behavior changes worth noting, neither contradicting a documented claim: (1) GetSessionDetails now computes session sales totals (totalSales, taxTotal, serviceChargeTotal, and the taxable/nontaxable net split) directly from the session's linked SalesOrders — including any still-open/unpaid orders, not just Cancelled ones — instead of from realized Invoices; this is a bug fix (previously, sessions whose orders weren't invoice-realized could show collapsed/zero totals) and makes the Layer-1 claim 'every order... attached to the session, subtotals... attached to the session' more literally accurate for the Sales Orders/Overview tabs of an in-progress session. Legacy sessions with orders never linked to the session still fall back to the invoice basis. (2) GetSessionHistory/GetSessionHistoryStats date-range filtering for the Sales Sessions list now buckets sessions by org-local calendar day (via a new ReportTimeZone helper) instead of a naive UTC .Date comparison — relevant to the 'same day or next morning' reconciliation-review guidance, since 'today's sessions' is now computed in the org's local timezone rather than UTC. Neither change touches the CloseRegister/ShowCashReconciliation/JoinSession/AcknowledgeCashHandovers/UpdatePaymentReconciliation code this entry's Layer 1-5 and Expected-formula claims are pinned to, and the diff was truncated before the full GetSessionDetails hunk (outstandingTotal calc) was visible, so this is logged as context rather than a body edit. (auto-applied by the truth pipeline; adversarially gated)
2026-05-04 — codebase: haafai/foodops.counter.app:b9677b0, haafai/haafai.app.foodops:46a80a3 (Views/SalesSessions/Details.cshtml, _Reconciliation.cshtml, _Payments.cshtml, _CloseRegister.cshtml confirmed); UI not yet verified
2026-06-03 — codebase: CHANGED (minor). Re-verified against hubits-api-sales@5274891. The five-layer reconciliation model is unchanged, but close is now a two-step Pending-Close → supervisor-approve flow rather than a single submit [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:1745-1753, 1894-1916]. Updated the "Closing a session that does not fully reconcile" section and one Common-question to add the approval gate. The "close is never blocked" framing still holds at the cashier-submit level.
2026-06-14 — codebase TRUTH AUDIT (hubits-api-sales@c445615, main). CORRECTED a material falsehood: the entry repeatedly claimed close is "never blocked" and that a session "can close with open tickets," surfacing the open ticket only as a post-hoc anomaly.
CloseRegisterin fact hard-rejects (HTTP 400, nothing persisted) on on-hold sales, open/non-terminal sales orders, and unposted/dead-lettered sales BEFORE any count is processed [code: SessionsController.cs:CloseRegister @1591-1649]. Rewrote the "Closing…" section, the "Will the system stop me" Q, the "Open tickets at close" edge case, Layer 1, the reconciled-definition paragraph, and the Anomaly key concept to reflect that open tickets are PREVENTED, only the cash variance flows to supervisor sign-off. Added citations for the variance threshold (>0.01) and Counted−(Opening+Expected) formula [@1776-1794], the cash-Expected = cash-payments − withdrawals computation [@1707-1745], delivery-cash acknowledge [@3406-3493], and per-payment IsReconciled flag [@4487-4508]. FLAGGED unverified: (a) cashier-of-record "Handover tab"/chain-of-custody (only delivery-cash handovers + Join/Leave found in this controller); (b) whether acknowledged delivery cash feeds the close-path Expected; (c) Receipts-tab UI filter on the Reconciled flag (frontend). Prior citation line numbers had drifted; re-pinned to symbol-anchored ranges.2026-06-29 — LAUNCH RE-VERIFY (new-stack only; legacy
haafai.app.foodopsfully retired). Re-pinned to foodops.counter.app@32669a9, hubits-api-sales@19190b9, foodops-admin-app@f6ffd0b; dropped retired haafai.app.foodops from source_repos (back-office reconciliation now lives in foodops-admin-app: RegisterSessionDetailsPage + SessionReconciliationReport + ApproveCloseDialog). NEW_INFO: a fourth close gate exists — transfer-reference ("blaze") issues, blocking close whenEnforceUniqueBlazeNumberis on (SessionsController.cs CloseRegister L1657-1672); added to the "Closing…" section, the "Will the system stop me" Q, and the Anomaly key concept. Close-gate range repinned to L1636-1700. The five-layer model, the open-order/held-sale/unposted hard-blocks, the variance formula + 0.01 tolerance, PendingClose two-step, and the Layer-5 no-chain-of-custody finding all re-confirmed unchanged. (Z-report referenced in Layers 2-5 is now a confirmed printable artefact — see z-report.md.)2026-06-15 — ADVERSARIAL re-verify of the 2026-06-14 corrections against hubits-api-sales main HEAD (read CloseRegister @1559-1909, ShowCashReconciliation @3561-3701, GetSessionDetails/SessionDetailsDto @909-1414, JoinSession @517-573, AcknowledgeCashHandovers @3407-3508, UpdatePaymentReconciliation @4447-4519, ApproveCloseRegister/CancelCloseRegister/AdjustRegister). UPHELD: the open-order/held-sale/unposted-sale close-block, the variance formula + 0.01 tolerance, cash-Expected = cash-payments minus withdrawals, the PendingClose two-step + supervisor approve/adjust/cancel, IsReconciled toggle. FIXED: (1) the pin
c445615does NOT exist in the repo (gh api 422) — repinned to real main HEAD2198276; (2) the "Cash Refunds net into the cash-payment sum" claim was unfounded — the close path applies NO refund term and does not separate Pay/Refund (the minus-refunds term lives only in show-cash-reconciliation @3683-3693); reworded both the Layer-3 bullet and the Expected-formula key concept; (3) Layer-5 confident "Handover tab logs every transfer / chain of custody — every minute attributable to a known cashier" was contradicted by the verified session-handover.md (Join sets LockedByUserId only, writes NO audit row; SessionDetailsDto @1343-1412 carries no per-handover history) — tightened Layer 5, its What-can-go-wrong, the reconciled-definition item 5, the Chain-of-custody and Handover-tab entries to reflect that only open/close cashiers + current lock holder are recorded. Tightened imprecise citation ranges (UpdatePaymentReconciliation to @4446-4519, AcknowledgeCashHandovers to @3406-3508).