Foodops
All docs

Counter · Updated 2026-08-21 · v2e5e3b0

Till Reconciliation

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

Plain-English summary

Till reconciliation is the act of counting the cash drawer at end of shift, comparing the count against what Foodops expects, and explaining any difference in writing. It happens on the Close Register form and is the last thing a cashier does before a shift closes. The key output is the variance — the gap between Expected and Counted — together with the cashier's Comments narrative explaining it. The reconciliation is what gives the day's cash a defensible audit trail.

When you'd use this

Till reconciliation is the close-of-shift ritual on every cash-accepting register. Cashiers run it when ending their day; managers run it when closing someone else's left-open session, when investigating a discrepancy that surfaced earlier, or when a register has to be reset for any reason. It also matters in training — explaining why the count must be denomination-by-denomination rather than a lump-sum total, and why the Comments field is not optional even when the variance is zero.

What "expected" actually means

Expected is the system's projection of what should be in the drawer right now. It is computed from the session's cash movements: [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:ShowCashReconciliation@main (expectedAmount = opening + payForCurrency − refundForCurrency − totalCashWithdrawals)]

Expected = Opening Balance
           + Cash Sales              (every successful cash Pay payment)
           − Cash Refunds            (cash Refund payments returned to a customer)
           − Cash Withdrawals        (manager pulls / drops recorded mid-shift)

Note: acknowledging a rider's delivery cash does NOT add to Expected. Ticking off a delivery-cash handover in the Handover tab only stamps it as acknowledged (audit/tracking) — it does not post a cash payment or change the drawer's Expected balance. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:AcknowledgeCashHandovers@main (only sets IsAcknowledged/AcknowledgedByUserId/AcknowledgedDate)]

This formula is mirrored on the Overview tab of Session Details, where each payment method shows its own Opening / Counted row, plus Expected / Difference columns only if the viewer holds the view-expected permission — a July-2026 "blind cash counting" change hides those two columns entirely for staff who don't. The reconciliation is the moment that abstract Expected number meets the physical drawer. [code: foodops.counter.app/src/pages/RegisterSessionDetailsPage.tsx@main (Expected/Difference <th>/<td> elements each wrapped in {canViewExpected && (...)})]

The reconciliation flow

  1. From the Counter screen, tap Details on the active session card → Session Details modal.
  2. Open the Close Register form. In the counter app this is a dialog (the Close Register modal) titled "Close ", showing a denomination table per currency and a Comments box; the Expected-cash card only renders if the cashier holds the view-expected permission — by default, staff count "blind" without seeing Expected while the register is open. [code: foodops.counter.app/src/components/session/CloseRegisterModal.tsx@main (canViewExpected = canViewExpectedCash(useUserPermissions()) gates CashDrawerExpectedCard)]
  3. Count the drawer. Enter each denomination's count in the Count column. The Amount column auto-computes (Count × Denomination); the Total row sums them.
  4. If you hold the view-expected permission, read the Expected figure off the Overview tab in the other open Session Details modal (or memorise it before navigating to Close). Staff without that permission count "blind" — no Expected figure is shown anywhere on the counter app until after the register closes. [code: foodops.counter.app/src/components/session/CloseRegisterModal.tsx@main (canViewExpectedCash); src/pages/RegisterSessionDetailsPage.tsx@main (canSeeExpectedForSession)]
  5. If Total matches Expected, the reconciliation is clean — variance is zero. Add a Comments note like "no variance — tally clean" and submit.
  6. If Total differs from Expected, write a specific Comments entry explaining the variance — see Comments specificity below.
  7. Tap Submit Count (this button was previously labelled Close Register) at the bottom of the form to commit. The session moves to Pending Close (not straight to Closed); the variance and Comments are written into the session record, and a supervisor then approves the close to make it final. See Pending Close and supervisor approval below. [code: foodops.counter.app/src/components/session/CloseRegisterModal.tsx@main ({loading ? 'Submitting...' : 'Submit Count'}, was {loading ? 'Closing...' : 'Close Register'})]

The system does not block submission on any variance — Foodops will accept the count with any drawer state, including a large unexplained shortfall. The protection here is procedural (the supervisor approval step + manager review of variance reports), not a variance limit.

Close does block on unfinished-work conditions, though — all must be cleared before the count is accepted: [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister@main (on-hold check L1636, open-orders check L1653, blaze gate L1657-1672, unposted-sales gate L1674-1700)]

  • On-hold sales — "Please unhold all sales before closing register."
  • Open / incomplete orders — "Please close all orders before closing register."
  • Transfer reference ("blaze") issues — only when the org enables EnforceUniqueBlazeNumber. If that setting is on and any bank-transfer payment in the session has a blank required reference or a duplicate reference, the close is rejected with "Resolve transfer reference (blaze) issues before closing the register." This is the server backstop for the counter's pre-close "Resolve transfer reference issues" modal. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister@main L1657-1672 (enforceBlaze && BlazeUniquenessHelper.SessionHasBlazeIssuesAsync); foodops.counter.app/src/components/session/SessionDetailsModal.tsx:377-385]
  • Sales still posting / failed posting — a session can't close while any of its sales are mid-post or dead-lettered. Pending items clear within seconds ("N sale(s) are still posting. Please retry in a few seconds."); dead-lettered items need back-office attention ("N sale(s) failed financial posting and need back-office attention before this register can close.").

Pending Close and supervisor approval

As of the 2026-05 close-flow rework, a cashier-driven close no longer goes straight to Closed. Every close lands in an intermediate Pending Close state (regardless of variance — variance only affects the response message), and a supervisor must explicitly approve it before the session is fully closed. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister@main (status set to PendingClose ~L1821, unconditional)]

  • The close response is "Register closed successfully" for a clean count, or "Register closed with variances. Approval may be required." when there is a non-zero difference (the variance lines are returned with it).
  • A supervisor approves the pending session (approve-close → Closed, stamping ApprovalClosedByUserId/ApprovalClosedTime), or cancels it (cancel-close → reverts to Open, deletes the Close-type cash counts and zeroes the balance Expected/Counted/Difference so the drawer can be re-counted). [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:ApproveCloseRegister@main ~L1979, CancelCloseRegister@main ~L2088]

Why denomination counts (not lump sums)

The reconciliation form forces a count per denomination — MVR 1000 row, MVR 500 row, MVR 100 row, and so on, plus a tab per currency in multi-currency outlets. Three reasons this matters:

  • Traceability. A denomination breakdown lets a manager spot patterns ("the till is short by exactly MVR 500 — one note missing, not coin slippage").
  • Anti-fudging. A single lump-sum field is trivial to back-fill with whatever number makes Difference zero. Forcing per-denomination entry makes a fabricated count harder to construct.
  • Float continuity. The next shift's opening float is sometimes carried forward from this denomination breakdown rather than re-counted, so the breakdown is operationally useful, not just audit theatre.

Why Comments specificity matters

The Comments field is the variance's only narrative. Managers triage variance reports by reading the Comments text first. Specific narratives like:

  • "Short MVR 50 — short-changed customer on SO-0004 cash payment"
  • "Extra MVR 100 — tip jar contents tipped into drawer at end of shift"
  • "Short MVR 200 — drawer left open during lunch rush, suspect petty cash use, will investigate"
  • "No variance — tally clean"

…are far more triageable than blank or generic ("close" / "ok") text. A specific narrative often closes an investigation before it starts; a vague one almost always escalates.

Reconciliation vs handover

Reconciliation only runs at session close. Mid-shift cashier changes use the session handover flow (the Join button), which only moves the session lock to the incoming cashier (LockedByUserId) and does NOT do a count — and, per the verified Session Handover entry, a Join writes no audit-log row either (only an application log line). [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:JoinSession@main (sets LockedByUserId/LockedTime only)] This is intentional: requiring a count at every shift change would slow service and fragment cash records across multiple partial sessions. The trade-off is that variance is computed for the whole shift's cash movement, not per cashier — and the system retains only who opened and closed the session plus the current lock holder, not a full per-handover chain of custody.

Key concepts

  • Reconciliation — the count-vs-expected comparison run at session close. Produces the variance figure and the Comments narrative.
  • Expected balance — Opening + cash sales − refunds − withdrawals + acknowledged delivery cash. The system's projection of drawer contents.
  • Counted balance — the physical cash counted into the Close Register form, summed per denomination. Becomes the session's Closing Balance.
  • Variance / Difference — Counted minus Expected (negative = short, positive = over). Zero is the target; non-zero requires Comments explanation. See cash variance for the manager-side handling.
  • Comments narrative — free-text explanation of the variance, written into the session's permanent record. Essential for downstream variance triage even when variance is zero.
  • Denomination count — a count per note/coin value rather than a single lump-sum total. Forced by the form structure for traceability and anti-fudging.

Common questions

Q: What if my count matches Expected exactly — do I still need a Comments entry? A: Yes, by convention. A short note like "no variance — tally clean" confirms the cashier checked rather than assumed. Auditors look at every session's Comments; an empty field is an open question.

Q: Will the system stop me from closing if my count differs from Expected? A: No — variance never blocks the count. But your close lands in Pending Close and a supervisor must approve it before the session is fully Closed (a close with a variance returns "Register closed with variances. Approval may be required."). Close does block, separately, if you still have on-hold sales, open orders, sales that are still posting / failed posting, or (when the org enables EnforceUniqueBlazeNumber) unresolved transfer-reference "blaze" issues — clear those first. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister@main]

Q: Can I edit the count after I tap Close Register? A: Once a supervisor has approved the close (session is fully Closed), the count is locked — the fix is then an out-of-band manager correction. But while the session is still in Pending Close, a supervisor can cancel-close, which reverts it to Open and clears the closing counts so the drawer can be re-counted and re-closed; or adjust-register, which replaces the closing counts and recalculates variance while staying in Pending Close. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CancelCloseRegister@main, AdjustRegister@main]

Q: I clicked Cancel by accident — are my counts saved? A: No, Cancel discards everything you typed. Re-navigate from Session Details → Close Register to start the count again. There is no draft state for in-progress reconciliations.

Q: The Amount column on the form stays at 0.00 even after I type counts. Is the form broken? A: No, the form isn't broken. In the counter app the Amount updates the instant you type (Amount = denomination × count, computed live). [code: foodops.counter.app/src/components/session/CloseRegisterModal.tsx:handleCountChange@main] In the older web (MVC) view the Amount cell updates when the Count field loses focus (tab/click away), not on every keystroke, so it can read 0.00 while you're still typing — click out of the field and it fills in. Either way the count is captured and the server stores Amount = Count × Denomination. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister@main (CountedAmount = cashCount.Count * cashCount.Denomination)] See Cash Handling for context.

Q: My outlet uses multiple currencies. Do I count each one? A: Yes — the form shows one tab per configured currency (MVR, USD, etc.). Each tab has its own denomination table. All tabs must be reviewed before submitting; the variance is computed per currency.

Q: Why does the Difference figure show on the Overview tab while the session is still open? A: Difference is only meaningful once you have entered Counted. The API returns the persisted DifferenceAmount (Counted − (Opening + Expected)), which is not populated until Close Register has been submitted with denomination counts. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:GetSessionDetails@main (DifferenceAmount = balance.DifferenceAmount) + CloseRegister@main (sets balance.DifferenceAmount on close)] In the counter app's Overview tab, while the session is still open both the Counted and Difference columns render a dash placeholder ("—"), not a number; they switch to the real figures only once the session is no longer open. [code: foodops.counter.app/src/pages/RegisterSessionDetailsPage.tsx@main (Overview table: session.isOpen ? '—' : formatMoney(...) for both the Counted and Difference cells)]

Q: Who can run a reconciliation on behalf of someone else? A: On the new stack there is no role or ownership gate on the close. The close endpoint is gated only by authentication ([ApiKeyOrSessionAuth]) and scoped to the caller's organisation — any authenticated user in the org can close (and approve/adjust/cancel) any open session, including one another cashier opened; there is no "you may only close your own session" rule and no Sales Registers: Edit permission check. The session records who ran the close (ClosedByUserId). [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister@main (class-level [ApiKeyOrSessionAuth], only an org-scope check, no [Authorize(Roles=...)])] (Operationally, organisations may still restrict who does this; that is policy, not an enforced code gate.)

Edge cases and known issues

Cashier counts the drawer twice and gets two different totals

  • Symptom: physical recounts diverge by small amounts.
  • Cause: typically miscount of low-denomination coins.
  • Workaround: count coins by stacks of 10 first, then sum. If divergence persists, write the lower count and explain in Comments — managers prefer an honest "uncertain by ~MVR 5" to a fabricated zero variance.

Expected appears wrong (e.g., shows MVR 0 for a busy shift)

  • Symptom: the Overview Expected column reads zero or implausibly low.
  • Cause: cash sales may not have flushed to the session's running totals if there were sync issues during the shift; or the cashier was looking at an unrelated payment-method row.
  • Workaround: cross-check against the Payments (and Transactions) tab in Session Details — every successful cash payment should be listed there. [code: foodops.counter.app/src/pages/RegisterSessionDetailsPage.tsx@main (tabs: Overview, Transactions, Payments, Pending Actions, Closing Count, Reconciliation, Handover — there is no "Receipts" tab)] If cash payments exist but Expected is zero, escalate before closing; closing now would lock in a wrong variance figure.

Close Register form refuses to submit

  • Symptom: tapping Close Register produces an error or no apparent action.
  • Cause: usually a network or session-token issue; occasionally a permission gate (a non-manager trying to close a manager-restricted session).
  • Workaround: refresh and retry. If the failure persists, capture a screenshot and escalate; do not abandon the count — once submitted, the data lives with the session and can be re-read.

Related

  • Sales Sessions — the session lifecycle that reconciliation closes

  • Sales Registers — the register the session ran on

  • Cash Handling — opening float, withdrawals, and the events that drive Expected

  • Cash Variance — the manager-side handling of non-zero Difference figures

  • Session Reconciliation — the broader close-shift accounting that till reconciliation is one piece of

  • Z-Report — the artefact printed at close, summarising the reconciliation

  • Session Handover — mid-shift cashier transfer that does NOT trigger reconciliation

  • Close Register form (internal-docs) — the screen this concept describes

  • 2026-08-14 — UPSTREAM (foodops-admin-app 4a70096..8f9feb5, 'feat(register-sessions): stale-session surfacing and the design's control row'; 'feat: add SessionOverviewPanels and SessionStaleNotice components with tests'): the admin back-office RegisterSessionDetailsPage now imports four new components — SessionOverviewPanels, SessionDrawerReconciliation, SessionStaleNotice, and SessionActionChecklist — replacing the old inline HeroStat cluster (deleted wholesale) and dropping the DaySummaryReportBody import (the adjacent comment changed from 'renders the Day Summary report' to 'renders the Day Summary figures'). A new facts = summarise(detail) memo feeds the header, stale notice and checklist, backed by new session-age helpers (isStale, daysSince, msSince, formatSpan, ranFor from utils/sessionAge) — a stale-session flag directly relevant to this entry's 'manager closing someone else's left-open session' scenario. A cross-tab search (tabSearch state + matches() filter over orders/invoices/payments) is also added. None of this touches ApproveCloseDialog.tsx or RegisterAdjustmentDialog.tsx (not in this diff), the backend Expected formula, the close gates, or the PendingClose/approval flow, so no existing documented claim is contradicted. The diff hunk is truncated mid-line and doesn't show SessionDrawerReconciliation's actual rendered content/placement or what condition marks a session 'stale' — worth a follow-up read of that component plus an admin-app Session Details screenshot before adding a dedicated subsection or Related-doc note. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-31 — UPSTREAM (foodops.counter.app 5468530..5ac2982, 'feat: session close-summary print plumbing — types, offline cache, broker mapper + submitSessionSummary'): Session Details now has two new header actions, 'Print Receipt' and 'Save PDF', both gated behind the same canViewExpected flag the entry already documents for the Overview tab's Expected/Difference columns (a code comment states the gate is 'closed → everyone; open/pending → only with the view-expected right', partially resolving the follow-up flagged in the 2026-07-30 note — though the body of canSeeExpectedForSession itself still isn't in this diff). 'Print Receipt' force-refreshes the close summary (sessionApiService.getCloseSummary(id, true), bypassing the 5-min cache) and routes it through a print broker to the device's current register printer; 'Save PDF' calls getCloseSummaryPdf(id) and downloads a blob via an anchor tag. Both buttons disable while offline (navigator.onLine), with a tooltip noting printing/saving needs a connection; thermal print additionally requires a configured broker. This is a genuine new user-facing capability directly relevant to the entry's existing 'Z-Report — the artefact printed at close' cross-reference, and to the 'defensible audit trail' framing generally — worth a dedicated subsection (or a Z-Report entry update) once the broker/PDF-generation internals and the actual on-screen placement (open vs. pending-close vs. closed) are screenshot-verified. Does not contradict any existing documented claim; the reconciliation formula, close gates, and PendingClose/approval flow are untouched by this diff. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-30 — UPSTREAM (foodops.counter.app c4cf5dc..5468530, 'feat: blind cash counting — hide expected/variance until register session close'; 'feat(counter): blind-close variance summary modal'): CloseRegisterModal now gates the Expected-cash card behind a new canViewExpectedCash permission check, and RegisterSessionDetailsPage's Overview tab now gates the Expected and Difference columns (headers, per-row cells, and totals row) behind canSeeExpectedForSession(userPermissions, session?.isClosed) — staff without that permission see no Expected figure and no Expected/Difference columns at all while counting, a genuine 'blind count' UX, not just the pre-existing open-session dash placeholder. The submit button is also renamed 'Close Register' → 'Submit Count' ('Closing...' → 'Submitting...' while in flight). The diff does not include the body of canViewExpectedCash/canSeeExpectedForSession (imported from @/hooks/useUserPermissions, not shown) or the new 'blind-close variance summary modal' named in the second commit subject, so exactly which roles retain Expected visibility, and what the reveal-on-close modal shows, is unconfirmed — needs a follow-up permission-matrix + screenshot check before the entry can fully describe who sees Expected and when. The backend Expected formula, the three/four close gates, and the PendingClose/supervisor-approval flow are untouched by this diff. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-19 — UPSTREAM (foodops.counter.app b43110b..054a5b3, 'fix(sessions): close stuck on "pending close" until another window opens'): the Session Details fetch is cache-first with a 5-minute TTL and no background revalidation, so after approve-close or cancel-close the supervisor's own screen could keep repainting the pre-write 'Pending Close' state until the cache aged out or another window happened to refresh the shared IndexedDB entry — this was a real, previously-unrecognised gap in the entry's documented approve-close/cancel-close flow, not a variance or state-machine issue. Fixed by forcing a network refetch (loadSessionDetails(true)) and purging the affected cache keys (/sessions/my, /sessions/{id}/details, /sessions/history) immediately after approve-close, cancel-close, recompute, and fix-duplicate-counts. Does not contradict any documented claim (the backend state transitions themselves — PendingClose→Closed/Open — are unchanged); it only means that, on pre-fix builds, a supervisor could reasonably (and incorrectly) believe an approval 'didn't take' when it actually had. Worth noting in case older client builds are still in the field, but no entry-body edit is warranted since this was a bug now resolved, not a documented-vs-actual contradiction. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-06 — UPSTREAM (hubits-api-sales 6f242ad..e31810a, 'feat(sessions): add operation-log audit coverage + history endpoint'): CloseRegister, CancelCloseRegister, AdjustRegister and OpenRegister now each write an OperationLog entry via session.Touch()/TouchCreate() (e.g. 'Session closed. ', 'Session close cancelled. ', 'Session adjusted. ', 'Register opened.'), and a new GET /sessions/{sessionId}/history endpoint returns the session's full audit timeline (action label, timestamp, actor, remarks, and any field-level diffs parsed from OperationLog.ExtraDetails). This is a new, previously-undocumented audit-trail capability directly relevant to reconciliation's 'defensible audit trail' framing and to the Comments narrative (Comments text is now persisted verbatim into the operation-log Remarks for close/cancel/adjust, not just onto the session record). Does NOT contradict the existing 'Reconciliation vs handover' claim that JoinSession writes no audit-log row — this diff does not touch JoinSession. Backend-only in this diff; no evidence yet of a frontend Session History tab/UI consuming the new endpoint, so treat as pending UI verification before adding a dedicated section — worth a follow-up check against foodops.counter.app / foodops-admin-app for a rendered history/timeline view. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-05-04 — codebase: haafai/foodops.counter.app:b9677b0, haafai/haafai.app.foodops:46a80a3 (Views/SalesSessions/_CashReconciliation.cshtml, _CloseRegister.cshtml, _ClosingCashDetails.cshtml confirmed); UI not yet verified

  • 2026-06-04 — codebase: hubits-api-sales@5274891 (SessionsController.cs close-register/approve-close/cancel-close). REAL CHANGES applied: (1) close now lands in PendingClose requiring supervisor approve-close (→ Closed) or cancel-close (→ Open, counts wiped) — added a "Pending Close and supervisor approval" section and updated the flow's final step + the edit/variance Q&A. (2) Close blocks on on-hold sales and open/incomplete orders (SessionsController.cs:1557,1574) — documented as the two hard gates. Variance still does not block. Denomination-count + Expected-formula behavior re-verified unchanged. Approval/pending UI not yet screenshotted.

  • 2026-06-14 — TRUTH AUDIT (codebase, prod branches): hubits-api-sales@f79eb7b, foodops.counter.app@df086e4, haafai.app.foodops@40624ca. CORRECTIONS: (1) Expected formula — removed fabricated "+ Delivery Cash Acked" term; acknowledging delivery cash only flips IsAcknowledged (AcknowledgeCashHandovers), it is NOT in Expected (ShowCashReconciliation: opening + pay − refund − withdrawals). Added citation. (2) Close gates — there are now THREE, not two: added the unposted/dead-lettered sales-posting gate. (3) "Full page (not a modal)" — WRONG; counter app uses CloseRegisterModal (a Dialog). Corrected. (4) "jQuery quirk, Amount stays 0.00, server recomputes" — WRONG/misleading: counter app updates Amount live on keystroke; legacy MVC view updates on field blur (not a permanent 0.00 bug). Rewrote. (5) "Manager closes any / Cashier only own session" — WRONG; closing is gated solely by the SalesRegisters.Edit permission with no ownership distinction (MVC SalesSessionsController.CloseRegister [Authorize]; API has only org-scope check). Corrected. (6) "Receipts tab" — no such tab; counter-app Session Details tabs are Overview/Transactions/Payments/Pending Actions/Closing Count/Reconciliation/Handover. Pointed to Payments/Transactions. (7) Flagged UNVERIFIED: whether Overview renders pre-close Difference as blank vs 0.00 (frontend rendering, not in code). Pins refreshed to prod (main) HEADs.

  • 2026-06-29 — LAUNCH RE-VERIFY (new-stack only; legacy haafai.app.foodops fully retired). Re-pinned to foodops.counter.app@32669a9, hubits-api-sales@19190b9, foodops-admin-app@f6ffd0b; dropped retired haafai.app.foodops from source_repos. NEW_INFO: added a fourth close gate — transfer-reference ("blaze") issues, blocking the close ("Resolve transfer reference (blaze) issues before closing the register.") only when the org enables EnforceUniqueBlazeNumber (SessionsController.cs CloseRegister L1657-1672; counter pre-close modal SessionDetailsModal.tsx:377-385). Close-gate line refs repinned (on-hold L1636, open-orders L1653, unposted L1674-1700). CORRECTED a now-invalid legacy claim: "closing is gated by Sales Registers: Edit" cited the retired MVC SalesSessionsController — on the new stack the close has no role gate (only [ApiKeyOrSessionAuth] + org-scope); back-office is now foodops-admin-app. Also fixed a stale handover claim (Join writes no audit row; no chain-of-custody from the Handover tab) to match the verified session-handover.md. Variance formula, 0.01 tolerance, PendingClose-on-every-close, CloseRegisterModal-is-a-dialog, live Amount-on-keystroke all re-confirmed unchanged.

  • 2026-06-15 — ADVERSARIAL RE-VERIFY (codebase, prod branches): hubits-api-sales@2458018, foodops.counter.app@df086e4, haafai.app.foodops@40624ca. Re-checked all 7 audit corrections against prod code with a refute bias. UPHELD (6/7): Expected formula + delivery-cash non-effect (ShowCashReconciliation L3693-3698, AcknowledgeCashHandovers L3483-3485); three close gates incl. unposted/dead-letter gate with verbatim messages (CloseRegister L1591/1606/1628); CloseRegisterModal is a Dialog, not full page (CloseRegisterModal.tsx L133-141); Amount-live-on-keystroke (handleCountChange L84-92); permission-gated-not-ownership close (MVC controller [Authorize]+SalesRegisters.Edit, no ownership check in CloseRegisterAsync; API [ApiKeyOrSessionAuth]+org-scope only); tab list confirmed verbatim with no Receipts tab (RegisterSessionDetailsPage.tsx L373-379). Also confirmed PendingClose set unconditionally (L1817-1821) and AdjustRegister stays PendingClose unconditionally despite its own stale docstring (L2320, L2337). RESOLVED the UNVERIFIED Difference flag: counter-app Overview renders "—" (em-dash) for Counted+Difference while session.isOpen, real value after close (RegisterSessionDetailsPage.tsx L442-445) — replaced the hedge with the grounded behavior. Corrected hubits-api-sales pin f79eb7b → 2458018 (the actual main HEAD the code was read at).

Go beyond POS.
Choose the total solution.

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