Plain-English summary
A zero-sale close is closing a register session that booked no orders during its open period. It happens after an accidental open, a no-show shift, or a test/training session that was opened but never used. The closing flow is exactly the same as a normal close — count the drawer, submit Comments, tap Close Register — but the count typically equals the opening float and the session record carries zero sales. The point is to leave a clean audit trail rather than just leaving the session open or trying to delete it.
When you'd use this
- A cashier opens a register by mistake (wrong button, wrong outlet) and realises before taking any orders.
- A shift was scheduled but the cashier did not arrive; another cashier opened on the wrong register and needs to close it before opening on the right one.
- A trainer demonstrates the open flow during onboarding and needs to close the practice session.
- A manager opens a session to test a screen change or printer setup and immediately needs to back out cleanly.
- An automated/scripted regression test opens a session and must close it as part of test teardown.
Why this is the right path (and not "delete the session")
Foodops does not provide a delete-session action — there is no delete-session endpoint on the API; the session lifecycle endpoints are open / close / approve-close / cancel-close (re-open) / adjust only. The session record cannot be removed once Open Register submits, and the audit log already has an entry showing who opened it and when. Two options remain:
- Close it. Run the Close Register flow. The session is retired with zero sales recorded, an opening-balance-equal-to-closing-balance reconciliation, and a Comments narrative explaining the situation. (Note the close API always sets the DB row to
PendingClose; on a clean zero-variance close the UI reports "closed successfully" and navigates away without invoking approve-close, so the row actually remains inPendingCloserather thanClosed— see "The flow" below.) [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs (no [HttpDelete]; only open-register/close-register/approve-close/cancel-close/adjust-register; close sets status → PendingClose at L1821, approve-close → Closed at L1979)] - Leave it open. Tomorrow it triggers the unclosed-session warning and someone has to close it cold anyway, with less context.
Closing immediately is always cleaner. The session is a tiny, easily-explained record rather than a confusing artefact someone else has to investigate later.
The flow
- From the Counter screen, tap Details on the empty session card → Session Details modal. The Sales Orders tab is empty; the Receipts tab is empty; Cash Withdrawal and Delivery Cash tabs are empty.
- Tap Close Register at the top-right of the modal → Close Register form.
- Count the drawer. In a clean zero-sale scenario, the drawer should still hold the opening float exactly. Enter the same denomination counts you entered at open — the Total should equal the Opening Balance.
- In the Comments field, write what happened. Be specific:
- "Opened in error — wrong register. Closing immediately, no transactions taken."
- "Training session for staff onboarding. Float counted out and back in unchanged."
- "Scheduled cashier no-show. Opened to validate setup; closing without taking orders."
- Tap Submit Count to submit (renamed from "Close Register"/"Closing..." to "Submit Count"/"Submitting..." by the 2026-07 blind cash counting change — applies to every user, regardless of permission). Behind the scenes the session row always lands in
PendingClosefirst (the close API sets this unconditionally, regardless of variance), and a separate approve-close step transitions it toClosed. But what you SEE depends on the count: on a clean zero-sale close with no variance (the float is intact), the API returns "Register closed successfully", the app clears the session, and you are NOT prompted for any approval — the close looks complete. The approval prompt ("Balance Difference Warning" → Close Anyway) only appears when there IS a cash difference AND the user holds the "view expected cash" permission (canViewExpectedCash). Users without that permission never see this prompt at all: on a variance, the app now auto-invokes approve-close in the background (approveBlindClose) and, only after the close has already committed, shows a read-only "Variance Summary" modal — with a retry/revert affordance if that background approve call fails. Whether a user clicks Close Anyway or the client auto-invokes it for them, the approve-close endpoint itself is still NOT gated to a supervisor or manager role — any user in the org can trigger it. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister (status set to PendingClose at L1821; no role check); foodops.counter.app/src/components/session/SessionDetailsModal.tsx:handleCloseRegister (no-variance → success toast + cleanup; variance → Close Anyway modal)]
The close-side summary on a zero-sale session
The close produces the session's reconciliation/summary output regardless of order count. On a zero-sale session it shows:
- Opening Balance equal to Closing Balance
- Cash Sales: 0.00
- Card / other payment method totals: 0.00
- Order count: 0
- Variance: 0.00 (assuming float is intact)
- Comments narrative
This artefact is operationally low-value — there is nothing to reconcile — but it provides a defensible audit record that a session existed, was clean, and was closed for a documented reason.
Edge cases worth flagging in Comments
A zero-sale close is not always a clean zero. Sometimes:
- The cashier counted the opening float wrong, then closes immediately on realising — the variance reflects the original miscount, not a real shortage. Comments: "Closing immediately to reset opening float — original count was wrong, see new session."
- The cashier accepted a single payment on a void/refund order and that is technically the only "transaction" — there are zero sales but a non-zero cash event.
- A test/training session may have created throwaway orders that need to be voided before close. Voids are still session events; the close is no longer truly zero-sale even if no money changed hands.
Each of these deserves a Comments narrative. The general rule: if anything happened on the session at all (counts, voids, prints), call it out.
Float-handling on a zero-sale close
The opening float is still operationally significant. It either:
- Stays in the drawer for the next session (most common). The next cashier opens with the same float, counts it again, and proceeds.
- Gets pulled out if the till is shutting down for the day (e.g., manager pockets the float, locks the drawer). Record this as a cash withdrawal before closing — otherwise the close-count will not match the Opening Balance, producing a phantom variance. (Close subtracts only recorded, non-deleted cash withdrawals from the expected base-currency balance; cash physically removed without a withdrawal record will not be subtracted, so the counted total comes up short and shows as variance.) [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister (withdrawals summed where RecordStatusId != Deleted, L1732-1735; expected = payments − withdrawals, L1740; difference = counted − (opening + expected), L1776)]
Forgetting to record a float pull is the single most common cause of variance on otherwise-clean zero-sale closes.
Permission gate
The close-register and approve-close API endpoints do not enforce a role-based or session-ownership gate. They require an authenticated org session (the controller carries [ApiKeyOrSessionAuth]) and scope every action to the caller's organisation, but there is no "cashiers can only close their own session / only managers can close others" check in the API or in the counter-app UI. In practice any authenticated user in the organisation can close (and approve the close of) any session in that organisation, including a session another cashier opened — which is what makes cleaning up someone else's accidental session possible. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister/ApproveCloseRegister (class-level [ApiKeyOrSessionAuth]; only org-scope check session.ApplicationOrganisationId != CallContext.ApplicationOrganisationId, no role/ownership gate)]
Key concepts
- Zero-sale close — closing a session that booked no orders. The flow is identical to a normal close; the count just equals the opening float and order count is zero.
- Opening Balance = Closing Balance — the natural state on a zero-sale close (assuming the float is intact). The variance is zero.
- No session delete — the session record cannot be deleted. Close is the way to retire it. (A close in progress CAN be backed out while it sits in
PendingClosevia cancel-close, which reverts it toOpenand wipes the close-attempt counts — but the session itself is never removed.) [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CancelCloseRegister (PendingClose → Open at L2114)] - Audit trail — the open and close events are recorded regardless of order activity. A zero-sale session still appears on the Sales Sessions list and in reports.
- Comments narrative — particularly important for zero-sale closes, because the absence of any other context makes the narrative the only explanation of the session's existence.
Common questions
Q: Can I delete a session I opened by mistake? A: No — sessions are immutable. The clean path is to close it immediately as a zero-sale close with a Comments note explaining the mistake.
Q: Does the system block close if there are zero orders?
A: No — close does not block on order count. It blocks only on: (1) on-hold invoices, (2) open/incomplete sales orders (not completed and not cancelled), (3) — only when the org enables EnforceUniqueBlazeNumber — unresolved transfer-reference ("blaze") issues, and (4) sales that are still posting financially or have dead-lettered. A zero-sale session has none of these (no orders means no transfer payments, so the blaze gate cannot fire either), so it closes. The session row then sits in PendingClose; on a clean (no-variance) close the cashier simply sees "Register closed successfully" and the session is cleared — there is no separate supervisor-approval prompt unless there is a cash variance. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister L1636-1700 (the close gates) + L1872 (status → PendingClose)]
Q: Will a zero-sale close affect daily reports? A: It appears on the Sales Sessions list with zero amounts and is captured in audit reports. It does not contribute to revenue figures. The Day Summary report includes every session that opened in the date window (no status filter) and computes revenue from invoices joined to those sessions — a zero-sale session has no invoices, so it shows in the session list but adds nothing to revenue. [code: hubits-api-sales/Hubits.Sales.Api/Services/Counter/Reports/DaySummaryReportService.cs (session query L115 filters only org + OpenedTime window, no status/IsOpen filter; revenue from invoice query L168 joined via sessionIds.Contains at L172)]
Q: Can I skip the count if the drawer obviously holds only the opening float?
A: The form does not technically force you to enter counts — every denomination defaults to zero and the Close Register button has no count-validation, so a submit with all-zero counts is accepted. But submitting zeros means the counted total reads 0.00 against the opening float, which produces a variance. So in practice you must still enter the same counts you used at open to get a clean zero-variance close. As policy, count it. [code: foodops.counter.app/src/components/session/CloseRegisterModal.tsx (denominations default count:0; Close Register button disabled={loading || currenciesLoading} only; handleConfirm passes counts/comments as-is, no count-required guard)]
Q: What if I accidentally open the wrong currency tab and submit zero counts on a multi-currency outlet? A: The submitted Closing Balance will be off by the missing currency's float. Variance will surface this. The fix is to count and submit honestly across all currency tabs; do not skip tabs to save time.
Q: Is a zero-sale close different from a "training mode" or a sandbox session?
A: There is no separate training/sandbox mode in the counter app or the sales API — every session is a real session that books to live data. A "training" session is just a normal session that the user closes as zero-sale afterwards. Comments should explicitly note the training context.
Q: Does the close-side summary still print on a zero-sale close?
A: The close-side summary/reconciliation output is part of the close, not gated on order activity, so it is produced on a zero-sale session too.
Q: Can I do a zero-sale close on someone else's accidental session?
A: Yes — any authenticated user in the same organisation can. The close/approve API only scopes by organisation; it does not restrict close to the session's original owner or to a manager role. The close record stores who closed it (ClosedByUserId), so the audit trail still shows it was closed on the original cashier's behalf. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister (org-scope check only, no role/ownership gate; sets session.ClosedByUserId = CallContext.UserId at L1819)]
Edge cases and known issues
Variance shows non-zero on a zero-sale close
- Symptom: for users with the "view expected cash" permission, the Difference column on the Overview tab is non-zero after a zero-sale close. Users without that permission don't see an Expected or Difference column on this screen at all (RegisterSessionDetailsPage.tsx hides both when
canViewExpectedis false) — for them the only place a variance surfaces is the post-close Variance Summary modal. - Cause: typically a miscount at open or close, or a float pull that was NOT recorded as a cash withdrawal (cash physically removed but no withdrawal record → counted total comes up short). Note: a withdrawal that was logged then deleted/voided does NOT cause variance — close excludes deleted withdrawals (
RecordStatusId == Deleted) from the expected-balance calculation. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister (withdrawal sum filters out RecordStatus.Deleted, L1732-1735)] - Workaround: investigate the Cash Withdrawal tab and the Comments narrative. If the Opening Balance was wrong, this close cannot fix it — the next session can re-open with the correct count.
Comments field empty on submit
- Symptom: the form accepts submit with no Comments text.
- Cause: Comments is not a required field — neither the close form nor the API validates it; the empty string is sent and accepted. [code: foodops.counter.app/src/components/session/CloseRegisterModal.tsx (Comments textarea has no required/validation; handleConfirm passes
commentsas-is); hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister (request.Comments assigned with no validation, L1820)] - Workaround: as a matter of policy, always write something even on a zero-sale close. A blank Comments field makes the session look unattended later.
Session appears on the Sales Sessions list with no Closing Balance
- Symptom: a previously-existing zero-sale session shows blank where Closing Balance should be.
- Cause: the close did not commit successfully (network failure, page navigation before submit completed). Close persists balances and status in a single SaveChanges, so a failed call leaves the session
Openwith no Close-type counts. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister (single SaveChangesAsync at L1823; close is rejected if session is not Open, L1582)] - Workaround: re-open Session Details, verify status is still Open, run Close Register again with current drawer state.
Related
Sales Sessions — the lifecycle that a zero-sale close still moves through
Sales Registers — the register the empty session was opened on
Session Lifecycle — formal state machine; zero-sale close is a normal Closing → Closed transition
Till Reconciliation — the close-shift count flow this entry overlays
Cash Handling — float, withdrawals; relevant when the float is pulled before close
Unclosed Sessions — the alternative state if you do not close an empty session
Z-Report — the artefact produced at close, even on zero-sale
Close Register form (internal-docs) — the screen where this happens
2026-08-22 — foodops.counter.app diff 8c411fc..83f0da4 (same commit subjects already logged 2026-08-17 for RegisterSessionDetailsPage.tsx, but this pull exposes additional SessionDetailsModal.tsx hunks not previously captured): NEW_INFO, not a contradiction. The already-known isVoidBinGate close-failure interceptor (first flagged 2026-08-04 as a close-blocking condition — 'clear the void bin' — absent from this entry's four-gate 'blocks only on' enumeration) has its regex broadened from /void bin/i to /(void|discard) bin/i, and its toast copy renamed from 'Review and clear the void bin...' to 'Review and clear the discard bin...'. This lines up with the 2026-08-17 note that RegisterSessionDetailsPage.tsx gained a new always-shown 'Discard Log' tab (DiscardLogTab component) — the product appears to be renaming the 'void bin' concept to 'discard bin' end-to-end, front-end only in this diff. No accompanying hubits-api-sales diff is supplied, so the backend condition's exact trigger and line number remain unconfirmed — per the standing 2026-08-04 guidance, do NOT rewrite the close-gate 'blocks only on (1)-(4)' enumeration until SessionsController.cs is re-pulled and this gate is cited with a line number. Separately, SessionDetailsModal.tsx gains a new QuickSwitchDropdown feature (session switching via new
joinSession/selectOpenSessionsplumbing, gated behind anenableQuickSessionSwitchper-session flag and the existing unsynced-exit confirm) letting a user jump directly to another of the org's open sessions without going through Close Register. This is adjacent to this entry's own 'opening the wrong register' scenario (When you'd use this) but describes a switch-without-closing path, not a close/zero-sale-close path — it does not alter or contradict any documented close mechanics. If quick-switch turns out to be the workflow cashiers actually use instead of a zero-sale close for the wrong-register case, that would be worth a forward-looking cross-reference, but nothing here confirms that yet. The remaining hunks (formatDurationSince/parseServerDate UTC fix, DiscardLogTab import, session-summary print salesRegisterId fallback, and the RegisterSessionDetailsPage.tsx loading/not-found/hero-band visual redesign) duplicate or extend the already-logged 2026-08-17 note and are cosmetic/unrelated to CloseRegister, variance, PendingClose, or role/ownership gating. (auto-applied by the truth pipeline; adversarially gated)2026-08-17 — foodops.counter.app diff 8c411fc..f30dbeb (fix(session): anchor the Running badge's start instant in UTC; other commits in the range — hide Take Out/Delivery on dine-in page, use real create permission in session order details, print manual receipt/KOT in org timezone — have no diff hunks intersecting this entry's cited surface and are unrelated to zero-sale-close behavior): NEW_INFO, not a contradiction. RegisterSessionDetailsPage.tsx (an existing cited source file for this entry) gains a new always-shown 'Discard Log' tab backed by a new DiscardLogTab component (sessionId + currencyCode props). This is thematically adjacent to — but not yet evidence for — this entry's own 'Edge cases worth flagging in Comments' claim that a nominally zero-sale session can contain throwaway orders that 'need to be voided before close' and that 'voids are still session events.' If DiscardLogTab turns out to list per-session void/discard events, it would be the first cited UI surface for that behavior; the diff supplied does not include the component's own source, so its actual contents are unconfirmed — do not add a specific claim about it until DiscardLogTab.tsx is pulled and read. Also in this diff but unconnected to any claim here: a cosmetic 'Customer' column added to the invoice table, an
onRefreshcallback wired into a tab inside SessionDetailsModal.tsx (component not named in the visible hunk), and a real bug fix to the session card's 'Running' duration badge —formatDurationSincenow anchors the open instant viaparseServerDate(treatsopenedTimeas UTC) instead ofnew Date(which read it in the terminal's local zone, inflating elapsed time by the terminal's UTC offset). That fix affects only the live 'Running' badge shown while a session is open, not any close-time, variance, PendingClose, or gating logic this entry documents. Nothing in this diff touches CloseRegister/ApproveCloseRegister, variance calculation, or role/ownership gating, so no existing claim is contradicted. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-08-14 — foodops-admin-app diff 4a70096..8f9feb5 (RegisterSessionDetailsPage.tsx, commit subjects: 'stale-session surfacing and the design's control row', 'add SessionOverviewPanels and SessionStaleNotice components with tests'): NEW_INFO, not a contradiction. The admin-app's Register Session Details page (already a cited source file for this entry) gains a new SessionStaleNotice component backed by new sessionAge utils (isStale/daysSince/msSince/formatSpan/ranFor), plus a SessionActionChecklist and SessionOverviewPanels/SessionDrawerReconciliation/SessionReconciliationReport replacing the old HeroStat cluster and DaySummaryReportBody. This looks like an admin-side surfacing of long-open/stale sessions — directly adjacent to this entry's 'Leave it open' alternative (currently documented only via the counter-app next-day unclosed-session warning) and its own 'When you'd use this' no-show-shift scenario, both of which describe exactly the kind of session a stale notice would flag. Also adds a client-side search/filter (tabSearch/matches()) across the Orders/Invoices/Payments tabs — cosmetic, no bearing on documented behavior. None of this touches CloseRegister/ApproveCloseRegister, variance calculation, or role/ownership gating (no hubits-api-sales diff accompanies this change), so no existing claim is contradicted. The diff supplied is truncated mid-hunk (cuts off inside the
filtereduseMemo), so the actual staleness threshold, the notice's copy/trigger condition, and whether it specifically calls out zero-sale sessions are unconfirmed — worth a full re-pull of RegisterSessionDetailsPage.tsx + SessionStaleNotice.tsx and a live screenshot before adding a specific claim to this entry or to unclosed-sessions.md. (auto-applied by the truth pipeline; adversarially gated)2026-08-09 — hubits-api-sales diff bb69312..7594413: NEW_INFO, not a contradiction. Fixes a double-subtraction bug in DaySummaryReportService.BuildReportAsync that fed the SessionCloseSummaryDto endpoint this entry logged as NEW_INFO on 2026-08-02 and whose UI wiring (print/PDF buttons) it logged on 2026-08-04. Previously the report subtracted cashWithdrawalTotal (summed across EVERY session in the report window) from each session's base-currency cash ExpectedAmount, even though the stored ExpectedAmount already nets that individual session's own withdrawals — per the new code comment, this 'made the Session Summary's Expected Cash disagree with the session details screen' on any day with more than one session, which is exactly the multi-register-mistake scenario this entry's own 'When you'd use this' section describes (opening the wrong register and needing to close it before opening the right one). The fix (labelled ADR-0011, 'single-source Expected Cash: pure reader') makes the report just read the stored ExpectedAmount as-is. A companion OpenRegister change now explicitly seeds ExpectedAmount to the opening float at open time ('with no sales and no withdrawals yet, what should be in the drawer IS the float') — consistent with, and reinforcing, this entry's existing citation that close-time variance is computed by netting recorded non-deleted withdrawals against a running Expected balance (SessionsController.cs:CloseRegister L1732-1735/1740/1776), which this diff does NOT touch. No claim in this entry asserted the old (buggy) multi-session behavior, so nothing needs correcting — but it's worth a source_repos addition for DaySummaryReportService.cs's ExpectedAmount shaping and a forward-looking edge-case note once confirmed live: a zero-sale close on a day with other open/closed sessions on other registers should now show an accurate (not leaked) Expected Cash on its printed Session Summary. Also note the SessionsController constructor now DI's DaySummaryReportService and IHtmlToPdfService directly, formalizing the close-summary/PDF dependency first surfaced 2026-08-02. (auto-applied by the truth pipeline; adversarially gated)
2026-08-04 — foodops.counter.app diff 5468530..37ad39c: NEW_INFO, not a contradiction. (1) RegisterSessionDetailsPage.tsx wires the close-summary artifact (SessionCloseSummaryDto/PDF, flagged in this entry's 2026-08-02 note as needing a UI-surface answer) to two new manual buttons — 'Print Receipt' (handlePrintThermal → sessionApiService.getCloseSummary(sessionId, true) → broker/thermal print) and 'Save PDF' (handleSavePdf → sessionApiService.getCloseSummaryPdf(sessionId) → browser download) — both gated behind the existing canViewExpected/blind-cash-count permission and both explicit user-initiated actions, not triggered automatically by close. This confirms rather than contradicts this entry's existing unverified-comment position that nothing auto-prints a 'Z-report' at close: printing/saving the summary is a separate explicit action, now named and located (RegisterSessionDetailsPage, not the Close Register modal). (2) SessionDetailsModal.tsx adds a new close-register failure branch, isVoidBinGate (
/void bin/i.test(message)), alongside the already-documented isFinancialPostingGate: on a matching close failure, the app closes the open dialogs and shows a toast with a 'Review' action that stashesfoodops_open_discardin sessionStorage and navigates to/sessions/{id}/dinein. This is first-party evidence of a close-blocking condition ('clear the void bin') not among the four gates this entry's close-gate Q&A currently lists as exhaustive ('blocks only on...(1)...(4)'). No accompanying hubits-api-sales diff was supplied, so the exact backend condition — and whether it can fire on a genuinely zero-sale session versus only one with voided orders (a scenario this entry's own 'Edge cases worth flagging in Comments' section already anticipates) — is unconfirmed. Do not rewrite the 'blocks only on' enumeration until SessionsController.cs is re-pulled and the void-bin gate is cited with a line number; in the meantime this is worth a follow-up edge-case note and a source_repos addition for RegisterSessionDetailsPage.tsx's print/PDF wiring. (auto-applied by the truth pipeline; adversarially gated)2026-08-02 — hubits-api-sales diff bb69312..df4179d: NEW_INFO, not a contradiction. Two new SessionsController endpoints ship a formal close artifact this entry previously could only gesture at as an 'unverified close-side summary': GET /sessions/{sessionId}/close-summary returns a new SessionCloseSummaryDto (tenders per payment-method x currency, sales counts, and the full Day Summary SalesTotals set), and GET /sessions/{sessionId}/close-summary/pdf server-renders the same data as a PDF (Figma 2850-20643 thermal layout) for the client-built 'Session Summary' thermal receipt. Both are explicitly built by re-invoking DaySummaryReportService.GenerateReport scoped to the single sessionId, so the figures are guaranteed to reconcile with the Day Summary report this entry already cites for revenue attribution. This still does not name anything a 'Z-report' (it's called 'Session Summary' throughout), so the entry's existing unverified 'no Z-report artifact' comments remain technically correct, but it meaningfully firms up the previously-uncited 'close-side summary is produced regardless of order count' claim: the endpoint has no order-count or session-status gate at the API level ('gating (closed-only) is a UI concern' per the doc comment) — it 404s only if the session doesn't exist, so it would return a valid (all-zero) summary for a genuine zero-sale session. Worth a source_repos addition (SessionCloseSummaryDto.cs, SessionCloseSummaryMapper) and a rewrite of the 'close-side summary' section + the two kb-claim:unverified HTML comments once confirmed live which UI surface (counter print action vs a new session-details button) actually calls close-summary/PDF for a real zero-sale close. Unrelated to this entry: the same diff also adds PosDefaultViewMode/SoftEnforcePaymentReference to SessionDto and a POS-layout/payment-reference settings read in GetMySessionsAndRegisters — pure POS-layout/payment-reference-enforcement additions, no bearing on zero-sale-close behavior. (auto-applied by the truth pipeline; adversarially gated)
2026-07-30 — foodops.counter.app diff c4cf5dc..5468530 (feat: blind cash counting — hide expected/variance until register session close; feat(counter): blind-close variance summary modal): adds a permission-gated 'blind cash counting' mode keyed on
canViewExpectedCash/canSeeExpectedForSession. Users WITHOUT that permission: (1) no longer see the pre-count 'Expected cash in drawer' card in the Close Register modal; (2) no longer see the Expected/Difference columns on RegisterSessionDetailsPage's Overview balances table; (3) on a variance close, never see the 'Balance Difference Warning' → Close Anyway prompt — the client auto-invokes approve-close in the background (approveBlindClose) and only then shows a read-only 'Variance Summary' modal (VarianceSummaryModal), with a retry/revert affordance if that background call fails. Users WITH the permission keep the previously-documented flow (manual Close Anyway prompt, visible Expected/Difference figures) unchanged. Separately and unconditionally for ALL users, the Close Register modal's submit button was renamed 'Close Register'/'Closing...' → 'Submit Count'/'Submitting...'. None of this touches the underlying API contract this entry cites (PendingClose-always, no role/ownership gate on close or approve-close, close doesn't block on order count) — it's purely a counter-app UI/permission change to who sees cash-reconciliation figures and who/what triggers the approve-close call. On a true zero-sale close (no variance) the success path is unaffected either way. Needs a live screenshot pass once confirmed which permission tier typical cashiers vs managers hold, to know which flow most real zero-sale-with-variance closes will actually go through. (auto-applied by the truth pipeline; adversarially gated)2026-07-27 — foodops.counter.app diff b43110b..c7d7801 (commit subject: fix(sessions): close stuck on 'pending close' until another window opens): SessionDetailsModal.tsx's post-close and post-cancel-close cache invalidation now also clears '/sessions/history' (a prefix delete covering the paged/filtered variants + /history/stats behind the Counter Sessions list). Previously that list cache (2-min TTL) could keep showing a just-closed or just-cancelled session in PendingClose until something else (e.g. another window) forced a refetch. This does not contradict any claim in this entry — the underlying state machine (row lands in PendingClose; no-variance close shows 'Register closed successfully' with no approval prompt; approve/Close Anyway is not role-gated; cancel-close reverts PendingClose→Open) is untouched by this diff, and SessionsController.cs (the backend source for all of that) is not part of this change set at all. The rest of the diff (a lifted paymentsExcluded filter state for the Payments tab, a useMemo around blazeIssueRows, and switching most read-only tab panels from always-mounted-hidden to mount-on-activate) is pure perf/state-plumbing and does not alter any documented zero-sale-close behavior. Worth a one-line mention in 'Edge cases and known issues' only if the pre-fix staleness was ever actually observed/reported against a zero-sale close in the wild — otherwise this is routine hygiene, not a new documented behavior. (auto-applied by the truth pipeline; adversarially gated)
2026-07-08 — foodops.counter.app diff 0545649..6885e57: SessionDetailsModal.tsx now intercepts close-register failures matching /financial posting|still posting/i (the dead-lettered/still-posting financial-posting gate this entry already cites at the API level in the close-gate Q&A) and routes them to a new FailedPostingsDialog with a retry action, instead of the generic toast.error this entry's flow-step-5 citation implied was the only failure path. This does not contradict any documented zero-sale claim: a true zero-sale session has no orders and therefore no financial postings to fail, so the gate structurally cannot fire for it, and the success-path / Balance-Difference-Warning behavior described in step 5 and the Permission gate section is unchanged. It is relevant to this entry's own 'Edge cases worth flagging in Comments' scenario — a nominally zero-sale session that contains voided/refunded throwaway orders — since those orders' postings could trip this gate, and the cashier would now see a retry dialog rather than a dead-end error. Worth a follow-up Q&A or edge-case note once confirmed live against counter.foodops.io. (auto-applied by the truth pipeline; adversarially gated)
2026-07-06 — hubits-api-sales diff 6f242ad..e31810a: NEW_INFO, not a contradiction. SessionsController.CloseRegister/CancelCloseRegister/AdjustRegister and OpenRegister now write an OperationLog entry via session.Touch()/TouchCreate() on every transition (open/close/cancel-close/adjust), each carrying the submitted Comments text. A new GET /sessions/{sessionId}/history endpoint returns this full audit timeline (Opened/Updated/etc., newest first) with per-field diffs parsed from OperationLog.ExtraDetails — directly evidencing this entry's existing 'Audit trail' and 'Comments narrative' claims, which previously had no code citation. The diff does not touch the PendingClose assignment, gating logic, or role/ownership checks documented elsewhere in this entry, so no existing claim is contradicted. Worth a follow-up note in Key Concepts/Related once the counter or admin UI surfaces a session history/timeline view backed by this endpoint. (auto-applied by the truth pipeline; adversarially gated)
2026-05-04 — codebase: haafai/foodops.counter.app:b9677b0, haafai/haafai.app.foodops:46a80a3 (Views/SalesSessions/_CloseRegister.cshtml confirmed; close flow does not gate on order count); UI not yet verified
2026-06-04 — codebase: hubits-api-sales@5274891 (SessionsController.cs). Re-verified: close still does NOT gate on order count (gates are on-hold sales + open/incomplete orders only, SessionsController.cs:1557,1574) — zero-sale close remains valid. REAL CHANGE applied: close now lands in
PendingCloserequiring supervisor approval beforeClosed(SessionsController.cs:1743) — updated the flow step and the zero-orders Q&A. UI not yet screenshotted.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 is now foodops-admin-app). NEW_INFO: the close-gate set is now four, not three — added the transfer-reference ("blaze") gate, active only when the org enablesEnforceUniqueBlazeNumber(a zero-sale session can never trip it: no orders → no transfer payments). Updated the "Does the system block close if there are zero orders?" Q and repinned its close-gate range to L1636-1700 and status→PendingClose to L1872. No-delete-session, no-role-gate, all-zero-count-accepted, deleted-withdrawal-excluded, and Day-Summary-includes-zero-sale-but-adds-no-revenue all re-confirmed; SessionsController.cs grew ~210 lines (blaze + edit-times) so other inline L-numbers in this entry now read low by ~50-210 lines but the symbol-anchored citations (CloseRegister/ApproveCloseRegister/CancelCloseRegister/AdjustRegister) remain valid.2026-06-14 — TRUTH AUDIT (codebase): hubits-api-sales@f79eb7b (main), foodops.counter.app@df086e4 (main). Corrections:
- Close gates: confirmed 3 gates — on-hold invoices, open/incomplete sales orders, AND unposted/dead-lettered sales (SessionsController.cs:CloseRegister L1591-1649). Added the third gate (was missing).
- PendingClose: confirmed the DB row always lands in
PendingClose(L1821). BUT the prior "supervisor approves it to finalise" framing was misleading for the zero-sale (no-variance) case: frontend shows "Register closed successfully" + clears the session with NO approval prompt unless there is a variance (SessionDetailsModal.tsx:handleCloseRegister). The approve/"Close Anyway" step is NOT role-gated to a supervisor — neither close-register nor approve-close has any role/ownership check; only org-scoping (no [Authorize(Roles)], no IsInRole). Rewrote flow step 5, zero-orders Q&A, and Permission gate. - Permission claims (Cashier-own vs Manager-any) were WRONG — no such enforcement exists in API or UI. Corrected.
- "Z-report" generated/printed at close — UNVERIFIABLE: no code path generates or auto-prints anything named a Z-report; real artifacts are Session Summary / Day Summary; counter-app printing is a separate explicit action. Flagged with UNVERIFIED comments; section renamed to "close-side summary"; frontmatter description de-asserted (tag + Related link to canonical z-report.md left intact).
- "form requires denomination entries" — PARTIALLY WRONG: CloseRegisterModal.tsx has no count-required validation; all-zero submit is accepted (but yields variance). Corrected.
- Day Summary includes zero-sale sessions (no status filter on session query) but contributes zero revenue (revenue from invoices joined on sessionIds) — CONFIRMED, cited (DaySummaryReportService.cs).
- No delete-session endpoint — CONFIRMED (only open/close/approve-close/cancel-close/adjust). Softened "immutable" to "cannot be deleted"; noted cancel-close can revert a PendingClose to Open.
- Deleted-withdrawal-causes-variance claim — WRONG: close excludes RecordStatus.Deleted withdrawals (L1732-1735). Corrected. UI not yet screenshotted.