Plain-English summary
Cash handling in Foodops covers every movement of money into or out of the drawer during a register session — from the opening float counted in at the start of a shift, through mid-shift withdrawals a manager pulls for a bank run, to the denomination-by-denomination count at close that produces the variance figure. Every cash event is logged against the session and is visible to anyone with access to that session's details. The closing variance — the difference between what the system expected in the drawer and what was physically counted — is the primary signal for end-of-shift reconciliation.
When you'd use this
- Opening a shift — entering the starting float denomination by denomination
- Mid-shift — a manager needs to pull cash from the drawer and log it so the expected balance stays accurate
- Delivery-enabled outlet — acknowledging cash returned by riders so it registers as cash-in on the session
- End of shift — counting the closing cash by denomination and understanding why the variance is what it is
- Investigating a discrepancy — tracing which cash event caused a mismatch between expected and counted
The opening float
When a cashier opens a register, they count the starting cash using the Open Register modal. The modal presents a denomination table — each row is one note or coin denomination, with a Count field and an auto-computing Amount column (Count × Denomination value).
The cashier counts the physical drawer contents, enters a count per denomination, and taps Open Register. The computed total becomes the session's Opening Balance — the baseline for all variance calculations during the shift.
Multi-currency outlets: the modal shows one tab per configured currency (MVR, USD, etc.), with the base currency selected by default. Each currency is counted on its own tab and all entered counts are submitted together. [code: foodops.counter.app/src/components/session/OpenRegisterModal.tsx:57-58; handleConfirm@111-123 iterates the full denominationCounts map across all currencies and POSTs them together] (Reviewing every currency tab before submitting is good practice but is not enforced — handleConfirm sends whatever counts were entered regardless of which tabs were viewed. There is also an "Open with Zero Balance" button, but it is only rendered as a fallback when the currency configuration fails to load, not as a general shortcut on the normal form. [code: foodops.counter.app/src/components/session/OpenRegisterModal.tsx:handleOpenWithZeroBalance@132, button rendered only inside the currenciesError branch @165-174])
Once the register is open there is no in-app field to change the Opening Balance — the practical fix for a wrong opening float is to close the (empty) session and re-open with the correct counts. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:OpenRegister@784]
Mid-session cash events
Cash withdrawals (pay-out)
A cash withdrawal is when a manager removes cash from the drawer mid-shift — a safe drop, a petty-cash payment, or a banking run. Recording the withdrawal keeps the Expected balance accurate so the closing count reflects reality.
To record a withdrawal:
- From the Counter screen, tap Details on the active session card → Session Details modal.
- Tap the Cash Withdrawal tab. (Session Details renders a dynamic tab bar — Overview, Sales Orders, Payments, Issues, Closing Count, Reconciliation, Delivery Cash, Cash Withdrawal, Handover, Online Orders — where several tabs only appear conditionally: e.g. the closing/reconciliation tabs only after a close is in progress, and the Issues tab only when the org enables
EnforceUniqueBlazeNumber(it lists invoices with missing/duplicate transfer-reference "blaze" numbers). Do not rely on a fixed tab position. [code: foodops.counter.app/src/components/session/SessionDetailsModal.tsx:46 (TabId union),395-397 (conditional show flags)]) - Tap New Withdrawal.
- Enter the amount, select a reason, and (optionally) add notes. The amount and reason are the meaningful inputs; notes is optional. [code: hubits-api-sales/Hubits.Sales.Api/Dto/Counter/Sessions/CashWithdrawalDtos.cs:CreateCashWithdrawalRequest]
- Submit. The withdrawal appears in the tab list and is reflected in the session's Expected balance.
Manager-authorization threshold. Each register carries a CashWithdrawalAuthorizationLimit. A withdrawal at or below the limit is recorded immediately. A withdrawal above the limit requires manager authorization: the server returns "Manager authorization required for amounts over …", an OTP must be requested and validated, and the cashier must submit the validated code before the withdrawal is recorded (the code is checked against a cached OTP that has not expired and was validated via the validate endpoint). The authorizing manager's name is then stored on the withdrawal. The withdrawal amount also cannot exceed the current drawer balance. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CreateCashWithdrawal@2788,2807,2851,2857]
A recorded withdrawal can be edited or deleted afterwards, but only while the session is still open — both the edit and delete endpoints reject the request when the session is no longer open (session == null || !session.IsOpen → "Cannot edit withdrawal from a closed session" / "Cannot delete withdrawal from a closed session"). [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:EditCashWithdrawal@3142-3144 (route POST cash-withdrawals/{id}),DeleteCashWithdrawal@3265-3267 (route POST cash-withdrawals/{id}/delete)]
The New Cash Withdrawal form collects: Withdrawal Amount (with quick-fill preset buttons 50, 100, 500, 1000 MVR), Reason for Withdrawal (dropdown sourced from the API reasons list: Petty Cash, Supply Purchase, Delivery Tips, Emergency Expense, Bank Deposit Preparation, Other), and Notes (optional free text). Current Drawer Balance is shown at the top. [code: foodops.counter.app/src/components/session/AddCashWithdrawalModal.tsx:395-400 (Current Drawer Balance),425 (quick-fill presets),473-480 (Notes labelled non-required, placeholder "Optional notes or details"); hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:GetWithdrawalReasons@2745-2750]

Withdrawals are visible to any user who can view the session's details. They carry through to the session's audit record after close.
Delivery cash (cash-in from riders)
When delivery orders use cash payment, riders collect money from customers and return it to the restaurant. The Delivery Cash tab in Session Details lists these incoming amounts per rider. Acknowledge receipt by ticking the relevant rows and tapping Acknowledge Selected.
Acknowledging delivery cash marks each selected handover as acknowledged (it records who acknowledged it and when) — it does not itself adjust the session's Expected balance. The acknowledge endpoint only flips IsAcknowledged on the selected handovers. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:AcknowledgeCashHandovers@3406,3483] What actually drives Expected for delivery cash is the underlying cash payment recorded against the delivery order: the close-register Expected calculation sums successful cash payments on the session and subtracts withdrawals; it does not read the delivery-cash-handover rows at all. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister@1700-1745]
The closing count and variance
At end of shift, the cashier navigates to the Close Register form via Session Details → Close Register. The form mirrors the Open Register modal: a denomination table where the cashier enters the physical count per denomination. The system computes the Total. Whether the cashier also sees the Expected figure while counting now depends on permission: as of a 2026-07 "blind cash counting" change, the Expected card on this form is rendered only for users with view-expected-cash permission ({canViewExpected && <CashDrawerExpectedCard .../>}); other users count blind and only see the comparison afterward, in a read-only Variance Summary shown post-submit.
The expected cash in the drawer is, in effect:
Expected drawer cash = Opening Balance
+ Cash Sales (successful cash payments in the session)
− Cash Refunds (cash refunded on orders)
− Cash Withdrawals (mid-shift withdrawals recorded in the tab)
Two code-level details matter for accuracy:
- Naming. Inside CloseRegister the variable the system calls
ExpectedAmountis the period movement only — successful cash payments minus withdrawals — and the Opening Balance is added back separately in the variance step. SoDifference = Counted − (Opening + ExpectedAmount), i.e.Counted − (Opening + cash sales − withdrawals). [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister@1740,1776] The cash-reconciliation-form endpoint reports anExpectedAmountthat does fold in Opening and explicitly subtracts cash refunds:Opening + cash Pay − cash Refund − withdrawals. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:3693,4091]
- The close path does not subtract refunds the way the reconciliation-form path does. Verified from code: at close, the cash Expected is
Sum(successful cash Payments) − withdrawals, where the payment query filters only byPaymentStatus=PaymentSuccessfull+PaymentMethod=Cashand does not filter onPaymentTypeIdat all — there is no separate− cash Refundterm. By contrast, both reconciliation-form endpoints split payments intoPaymentType.PayandPaymentType.Refundand computeOpening + Pay − Refund − withdrawals. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister payment query@1700-1705 (no PaymentType filter); show-cash-reconciliation@3677,3684,3693; save-cash-reconciliation@4026,4043,4091] - Delivery cash is not a separate Expected line. There is no "+ Delivery Cash Acknowledged" term — delivery cash only affects Expected through the underlying cash Payment on the delivery order, not through the handover-acknowledgement rows (see Delivery cash).
The Difference is the variance, computed as Counted minus (Opening + period movement). A negative Difference means the drawer has less than expected (short); a positive Difference means the drawer has more than expected (over). The reconciliation-details view shows the signed Difference (rendered in red when it is negative); it is not relabelled as an absolute "short/over" figure. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister@1776; foodops.counter.app/src/components/session/ViewReconciliationDetailsModal.tsx:321-327]
The Comments field on the close form is the cashier's explanation of any variance. Specificity matters for manager triage:
- "Short MVR 50 — suspect change error on SO-0004"
- "Extra MVR 100 from tip jar, not recorded as a sale"
- "No variance — tally clean"
A cashier-driven close does not require a clean tally to go through, but it does not finalise the session on its own either: the persisted session status is set to PendingClose on every close regardless of whether there was a variance, and a supervisor must explicitly run the approve-close step before the session record is fully Closed. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister@1821 (SalesRegisterSessionStatusId = ...PendingClose set unconditionally),ApproveCloseRegister@1914,AdjustRegister@2148] From PendingClose a supervisor can approve the close, adjust/recount the closing cash (AdjustRegister), or cancel the close (CancelCloseRegister@2061). The user-facing wording differs by variance: a variance returns "Register closed with variances. Approval may be required." while a clean close returns "Register closed successfully" — and the SignalR/cross-app broadcast even labels a clean close as status "Closed" (hasDifferences ? "PendingClose" : "Closed") — but in both cases the stored status is PendingClose, so even a clean-tally close still awaits supervisor approve-close. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:1834,1873,1893-1895]
Common variance reasons
| Type | Typical cause |
|---|---|
| Short — drawer has less than expected | Change given incorrectly; void after cash collected; unrecorded cash removal |
| Over — drawer has more than expected | Change shortchanged to customer; tip in drawer not recorded as income; incorrect opening float |
| Zero variance but wrong absolute total | Opening float entered wrong; two offsetting errors netting to zero |
| Large unexplained variance | Possible theft or serious miscount; escalate to management for investigation |
Key concepts
- Opening balance / float — the cash loaded into the drawer at session open, entered by denomination. The baseline for Expected calculations.
- Expected balance — the system-computed cash total: Opening + cash sales − refunds − withdrawals. There is no separate "+ acknowledged delivery cash" term; delivery cash enters only via the cash Payment on the delivery order. Note the in-code
ExpectedAmountat close excludes Opening (it is period movement only — see The closing count and variance). Shown per payment method in the Session Details Overview tab as the Expected column. - Counted balance — the physical cash counted at close, entered by denomination on the Close Register form. Becomes the Closing Balance on the session record.
- Variance / Difference — Counted minus Expected (negative = short, positive = over). The reconciliation signal. Zero is the target; non-zero triggers review.
- Cash withdrawal — a manager-recorded removal of cash from the drawer mid-shift. Logged in the Cash Withdrawal tab; reduces Expected.
- Delivery cash — cash from delivery orders returned by riders to the register. Acknowledged in the Delivery Cash tab; increases Expected.
- Denomination count — entering counts per note/coin value rather than a lump-sum total. Provides a traceable breakdown and supports denomination-level discrepancy investigation.
Common questions
Q: Can I enter a zero opening float? A: Yes — OpenRegister does not reject a zero (or empty) opening count; it just sets the Opening Balance to that total. There will simply be no cash in the drawer to make change. Confirm with management whether a zero-float shift is permitted for the outlet. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:OpenRegister@778-790] See Open Register.
Q: I entered the wrong opening float and the session is already open. What do I do?
A: If no orders have been taken, close the session via Close Register (producing a zero-sale close) and re-open with the correct denominations. If orders have been taken, there is no in-app way to change the Opening Balance on an open session — continue the shift, and the wrong float will surface as an offsetting cash variance at close; add a note in the close Comments explaining it, and tell your manager so the variance is expected at reconciliation. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:OpenRegister@784 — opening balance is set at open only; no update endpoint]
Q: Who can record a cash withdrawal? A: Any user with an open session on the register can initiate a withdrawal — there is no UI-level role gate on the "New Withdrawal" button. The gate is amount-based: a withdrawal at or below the register's authorization limit goes through immediately; a withdrawal above the limit requires manager OTP authorization before it is recorded (see the threshold note in Mid-session cash events). [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:2739] Verified 2026-05-04 (E2E): the E2E test account (standard user, not a dedicated Manager role) could see and access the "New Withdrawal" button and submit. The "New Cash Withdrawal" form shows: Current Drawer Balance (info), Withdrawal Amount (required, MVR prefix + quick-fill buttons — current prod code shows 50/100/500/1000, not the 100/200/500/1000 recorded at the 2026-05-04 E2E [code: foodops.counter.app/src/components/session/AddCashWithdrawalModal.tsx:425]), Reason for Withdrawal (required dropdown: Petty Cash / Supply Purchase / Delivery Tips / Emergency Expense / Bank Deposit Preparation / Other), Notes (optional), Save / Cancel (screenshot 176). The E2E pass did not exceed the authorization limit, so the OTP path was not exercised.

Q: Do cash withdrawals automatically reduce the Expected balance? A: Yes — at close, the sum of all non-deleted withdrawals on the session is subtracted from the cash Expected for the base currency. If you pull MVR 500 mid-shift and record it, Expected at close will be MVR 500 lower than if you had not recorded it. If you pull the cash but forget to record it, the drawer will appear short by that amount at close. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister@1732-1740]
Q: What happens if I close without acknowledging delivery cash?
A: Acknowledging a handover does not itself move the Expected balance — the close calculation reads cash Payments and withdrawals, not the handover-acknowledgement rows. Operationally, acknowledge all delivery cash in the Delivery Cash tab before closing so the rider cash is reconciled and so its cash Payment is in place; if drawer cash is present but the corresponding cash Payment was never recorded, it will surface as an unexplained over at close.
Edge cases and known issues
Opening float entered as a lump sum into one denomination row
- Symptom: cashier types "500" into the MVR 500 row instead of spreading counts across all denominations present.
- Result: Opening Balance is numerically correct but the denomination breakdown is wrong. This does not affect the variance calculation but corrupts denomination-level float reports if those are used.
- Workaround: training. The Close Register form uses the same denomination approach, which usually makes the correct method clear by end of shift.
Cash withdrawal recorded with a wrong amount
- Symptom: withdrawal submitted; cashier notices the amount is incorrect.
- Workaround: edit or delete the withdrawal directly from the Cash Withdrawal tab's Actions column while the session is still open — the API exposes both an edit and a delete endpoint for a recorded withdrawal, gated on the session being open. Once the session closes, the withdrawal can no longer be edited or deleted; correct it out-of-band via a Manager note. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:2910,3039] . Do not close with a known bad withdrawal amount you cannot fix — it will produce a misleading variance that must be manually explained.
Delivery Cash tab shows no rows even though delivery orders exist
- Symptom: the tab is empty despite delivery orders showing in the Sales Orders tab.
- Cause: expected behavior. The tab lists delivery-cash-handover records for the session (each row carries handed-over / acknowledged flags); it is not a list of every delivery order. If no handover records exist for the session — e.g. delivery orders were card-paid, or no rider cash-handover has been created yet — the tab shows "No delivery cash handovers found for this session". [code: foodops.counter.app/src/components/session/SessionDeliveryCashTab.tsx:150,187; hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:991]
Related
Sales Sessions — the session lifecycle that all cash events belong to
Sales Registers — the register is what a session (and its cash) is anchored to
Session Handover — passing a session to another cashier; no cash count at handover
Close Paid Order — order-level payment vs session-level cash reconciliation
2026-08-20 — hubits-api-sales (98c202d..c7e4894) adds server-side support for the session discard/void log surfaced in foodops.counter.app's new DiscardLogTab.tsx/DiscardOrderLogModal.tsx (flagged provisional in the same-day counter.app verification-history note above): both close-summary endpoints (GetSessionCloseSummary, GetSessionCloseSummaryPdf) now populate a new SessionCloseSummaryDto.DiscardLog field via SessionDiscardLogService.BuildAsync(...), plus a new SalesRegisterId field looked up per-session. The new SessionDiscardLogDto shape (OrderCount, TotalQuantity, TotalAmount, and per-row OrderId/OrderReference/VoidedDate/OrderType/Reason/RequestedBy/ReviewedBy/TotalAmount/Status) matches the stat-card/table shape described in the counter.app note, confirming the discard log is wired into the Session Close Summary — not necessarily into the Session Details tab bar; SessionDiscardLogService's method body and any UI consumer of this new DTO field are not in this diff. Separately, the CloseRegister void-bin pre-close gate's comment changed from '(approved/rejected)' to '(approved/rejected/acknowledged)', consistent with this commit range's 'void bin approval mode + acknowledge review path' subject line — but the visible hunk only changes the comment text, not the underlying VoidBinEntries filter/VoidReviewStatus enum, so a new 'acknowledged' review state is suggested by naming only and is NOT confirmed from a method body; worth a follow-up read once the filter/query body is visible. Neither change touches or contradicts this entry's documented cash-handling logic (Expected/Counted/Difference formula, withdrawal OTP threshold, unconditional PendingClose-on-close, delivery-cash acknowledge-only-flips-flag) — additive only. (auto-applied by the truth pipeline; adversarially gated)
2026-08-20 — foodops.counter.app (8c411fc..fe34047) adds two new components inside this entry's tracked surface (src/components/session/**): DiscardLogTab.tsx and DiscardOrderLogModal.tsx. Together they implement what looks like the 'void bin' review UI flagged as unconfirmed in the 2026-08-04 verification-history notes — a per-session discard/rejection log (Total No of Orders / Total Amount stat cards; a table of Order ID, Date & Time rendered in the org timezone, Order Type, Reason, Requested By, Reviewed By, Total, and a color-coded Status pill for Approved/Rejected/Approval Pending) plus a click-through modal that merges the order's OperationLog history with synthetic Discard Requested/Approved/Rejected timeline events. This is additive and does not contradict any of this entry's documented cash-handling logic (variance formula, withdrawal OTP threshold, unconditional PendingClose-on-close, delivery-cash acknowledge-only-flips-flag) — a discard/void log is a distinct concern from the Expected/Counted/Difference cash math. NOT confirmed: this diff's only SessionDetailsModal.tsx hunk adds an unrelated onRefresh callback to the Sales Orders tab render — it does not touch the TabId union or the conditional tab-show flags previously cited (:46, :395-397) — so there is no direct evidence here that a 'Discard Log' tab is actually reachable from the Session Details tab bar yet; the components may be shipped in the bundle but not yet wired into the UI. Also notable: this landing isn't reflected in the diff's own commit-subject list (dine-in button hiding, order create-permission, receipt/KOT timezone, Running-badge-UTC fixes only), so treat the discard-log feature itself as provisional pending a direct SessionDetailsModal tab-bar diff and the corresponding hubits-api-sales endpoint on its prod branch before adding a dedicated subsection. (auto-applied by the truth pipeline; adversarially gated)
2026-08-08 — hubits-api-sales (144add7..cdb3b32) changes OpenRegister so the persisted SalesRegisterSessionBalance.ExpectedAmount is seeded to the opening float amount at creation (both the multi-currency loop and the single-currency fallback branch), instead of being left at its implicit zero default until the first payment settles and a later recompute runs. Per the added code comment, this fixes a real gap: a just-opened register previously showed Expected as 0.00 (not the opening float) until the first payment triggered an UpdateTotals recompute. This is consistent with — not a contradiction of — this entry's documented formula (Expected = Opening + cash sales − refunds − withdrawals, which reduces to Expected = Opening with no sales/refunds/withdrawals yet); it only changes when that value is written to the balance row shown in the Session Details Overview 'Expected' column, so no edit to the documented formula is needed. The diff also adds an XML-doc comment (method body not included in this diff) for a new private refresh helper that defers to a 'single authoritative definition' — SessionExpectedAmountCalculator — meant to unify Expected computation across the debounced open-session recompute and the close path, because Hangfire-debounced recompute can lag a payment that settles seconds before close, and CloseRegister can't call the existing UpdateTotals directly (it would try to re-insert already-tracked payments via EF). This hints at a larger refactor that could eventually touch the already-flagged-unverified claim that the close path doesn't subtract refunds the way the reconciliation-form path does — but the actual method body and any change to CloseRegister's payment query are not present in this diff, so that remains unconfirmed and worth a follow-up read once SessionExpectedAmountCalculator lands. Unrelated to cash handling: this same diff range also adds PosDefaultViewMode/SoftEnforcePaymentReference to SessionDto (POS layout + transfer-reference-enforcement settings) and switches GetMySessionsAndRegisters' OpenedTime to an explicit UTC ISO-8601 string (fixing a timezone display bug), neither of which touch cash flow. (auto-applied by the truth pipeline; adversarially gated)
2026-08-04 — hubits-api-sales (144add7..0c16730) confirms the server side of the 'void bin' pre-close gate that the 2026-08-04 foodops.counter.app note (a00083c..37ad39c) flagged as unconfirmed. CloseRegister now queries VoidBinEntries for the session (SalesRegisterSessionId == session.Id, org-scoped) filtered to ReviewStatusId == VoidReviewStatus.Pending; if any exist it short-circuits with BadRequest { Success = false, Message = "Review and clear the void bin before closing the register." } — an exact string match for the counter.app isVoidBinGate() /void bin/i toast text documented previously. The check runs before the existing blaze-number validation in the same method, i.e. it's a distinct, earlier-evaluated gate alongside the already-documented variance/PendingClose, financial-posting, and blaze-issues gates. Still unconfirmed: the enableVoidBin flag's rollout/default (this diff doesn't show a feature-flag check gating the query, implying it may be unconditional server-side even if the client flag is still rolling out — worth double-checking), and the void-bin review UI on /sessions/{id}/dinein (not in this diff). Unrelated to cash handling: this same diff also adds PosDefaultViewMode/SoftEnforcePaymentReference fields to SessionDto via GetMySessionsAndRegisters (POS layout + transfer-reference-enforcement settings, no cash-flow impact). None of this contradicts the entry's documented claims (variance formula, withdrawal OTP threshold, unconditional PendingClose-on-close, delivery-cash acknowledge-only-flips-flag) — additive only, and it upgrades the void-bin gate from 'client-only, API side unconfirmed' to 'confirmed both sides, message text matches exactly.' (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-08-04 — foodops.counter.app (a00083c..37ad39c) adds a new pre-close gate distinct from the variance/PendingClose, financial-posting, and blaze-issues gates already documented: a 'void bin' feature (feat: void bin offline plumbing — enableVoidBin flag + Dexie v26 voidBinEntries + sync actions) intercepts a CloseRegister failure whose message matches /void bin/i via a new isVoidBinGate() check in SessionDetailsModal.tsx. On match it closes the active Close Register and Session Details dialogs and shows a toast — 'Review and clear the void bin before closing the register.' — with a 'Review' action that stashes the session id in sessionStorage (foodops_open_discard) and navigates to /sessions/{id}/dinein, presumably to a void-bin review UI on the dine-in screen (not present in this diff). Per the new SessionDetailsModal.forceClose.test.tsx case, this gate takes precedence over the existing blind-cash-counting auto-approve path documented in the 2026-07-29 verification-history note (approveBlindClose is NOT called) even for a user without view-expected permission — the void-bin block wins regardless of role. This diff only touches counter.app (SessionPaymentsTab.tsx, sessionSlice.ts, SessionDetailsModal.tsx + tests); the corresponding hubits-api-sales CloseRegister change that would emit the void-bin message, the enableVoidBin flag's rollout/default, and the actual void-bin review UI on /sessions/{id}/dinein are not in this diff and remain unconfirmed. The other two touched files (SessionPaymentsTab.tsx's new admin-payment-detail link on the transaction reference, and sessionSlice.ts's added persistence of posDefaultViewMode/softEnforcePaymentReference across offline hard-refresh) are unrelated to cash-handling logic. None of this contradicts the entry's documented claims (variance formula, withdrawal OTP threshold, unconditional PendingClose-on-close, delivery-cash acknowledge-only-flips-flag) — additive only. Worth a dedicated 'void bin' pre-close gate subsection once the hubits-api-sales side and the dine-in review UI are confirmed on their prod branches. (auto-applied by the truth pipeline; adversarially gated)
2026-08-01 — hubits-api-sales (bb69312..0d94f42) added a read-only 'Session Close Summary' feature layered on top of session close: two new endpoints, GET /{sessionId}/close-summary and GET /{sessionId}/close-summary/pdf, both built by projecting the existing DaySummaryReportService.GenerateReport(...,sessionId) output — the same report generator the Day Summary receipt uses — so the new 'Session Summary' thermal receipt/PDF is defined to reconcile with that report by construction. The DTO (SessionCloseSummaryDto) exposes one Opening/Expected/Counted/Difference tender row per payment-method×currency plus the full ordered Day Summary SalesTotals line set (Discounts, FOC, Refunds, Tax, Service Charge, Delivery Charge, Cash Withdrawals, Total Amount, Total Sales, Outstanding, Received Amount). This is purely additive: it does not touch CloseRegister, CreateCashWithdrawal, AcknowledgeCashHandovers, OpenRegister, ApproveCloseRegister, or AdjustRegister, and none of this entry's documented logic (variance formula, OTP withdrawal threshold, unconditional PendingClose-on-close, acknowledge-only-flips-flag) changed. No counter.app UI change is present in this diff, so it's unconfirmed how/where the frontend surfaces this (a print/download action on Close Register or Session Details is the likely fit, matching the Figma reference 'thermal layout' in the PDF endpoint's doc comment). Worth a short 'Session close summary' addition once the counter.app prod branch shows the calling UI, plus confirming whether
?download=trueis user-toggleable or fixed per surface. (auto-applied by the truth pipeline; adversarially gated)2026-07-31 — foodops.counter.app (5468530..5ac2982) adds a new session close-summary retrieval/print service: sessionApiService.getCloseSummary(sessionId, forceRefresh) fetches a 'Session Summary' DTO for thermal print, offline-cached via bootstrapCache.saveSessionCloseSummary/loadSessionCloseSummary (same offline-first pattern as getSessionDetails), getCachedCloseSummary() reads that cache directly, and getCloseSummaryPdf(sessionId) fetches a server-rendered PDF blob from a new GET /sessions/{id}/close-summary/pdf endpoint. Commit subjects also reference 'session close-summary print plumbing — types, offline cache, broker mapper + submitSessionSummary', implying a thermal-print broker consumes this DTO, but that mapper and any UI trigger (e.g. a print/download control in Session Details or on the Close Register success path) are not in this diff (only src/services/session/** is touched here) and remain unconfirmed. This is additive — it does not touch or contradict the documented variance formula, withdrawal OTP gate, unconditional PendingClose-on-close, or delivery-cash acknowledge-only-flips-flag logic already in this entry. Worth a dedicated 'session close summary / print' subsection once the broker mapper, the UI print/PDF trigger, and any corresponding hubits-api-sales close-summary endpoint are checked on their prod branches. (auto-applied by the truth pipeline; adversarially gated)
2026-07-29 — foodops.counter.app (c4cf5dc..5468530) shipped a "blind cash counting" feature: a new
canViewExpectedCash/canSeeExpectedForSession(perms, isClosed)permission gate now hides the Expected Amount and Difference/Variance figures (Counted stays visible) across SessionOverviewTab, SessionBalancesTab, SessionHandoverTab, SessionReconciliationTab, CashReconciliationModal, and the Close Register form's CashDrawerExpectedCard — hidden from unprivileged users while the session is open, and shown again once it's closed (theisClosedparam). The Close Register modal's submit button was also relabelled 'Close Register'/'Closing...' → 'Submit Count'/'Submitting...'. Most significantly, SessionDetailsModal.tsx adds a new closing branch: when a user without view-expected permission closes a session that has a variance, the app no longer shows the existing balance-differences review dialog — instead a newapproveBlindClose()helper calls the approve-close endpoint automatically, then displays a new (not included in this diff)VarianceSummaryModalrecap before returning to session details. This looks like it changes the documented 'a supervisor must explicitly run the approve-close step' behavior for blind-count users, but this diff only touches the counter.app frontend — no hubits-api-sales change is included, so it's unconfirmed whether the approve-close endpoint independently enforces a manager/supervisor role server-side for this auto-triggered call. Also unconfirmed: whether the CashReconciliationModal.tsx touched here is the same surface as the already-documented 'reconciliation-details view' (currently cited to the separate, unmodified ViewReconciliationDetailsModal.tsx). Recommend a dedicated 'blind cash counting' subsection once the hubits-api-sales prod branch and VarianceSummaryModal are checked, plus a role-based screenshot pass (privileged vs. unprivileged cashier) before finalizing edits beyond the two applied above. (auto-applied by the truth pipeline; adversarially gated)2026-07-08 — foodops.counter.app (0545649..6885e57) added a new pre-close gate distinct from the variance/PendingClose flow: if Close Register fails because one or more of the session's sales failed async financial posting (invoice/payments/journals/stock dead-lettered after exhausting retries) or are still mid-posting, sessionApiService now translates the server's structured 400 { success:false, message } body into a normal result instead of throwing, SessionDetailsModal detects the message via
/financial posting|still posting/i, and opens a new FailedPostingsDialog (backed by a new salesPostingService client hitting GET /v1/sales/posting/dead-letter and POST /v1/sales/posting/{id}/retry) listing the blocking sales (order/invoice ref, attempt count, last error) with per-row and 'retry all' requeue actions; a clean 'retry all' closes the dialog so the cashier can attempt Close Register again. This is additive and does not touch the documented variance formula, withdrawal OTP gate, or unconditional PendingClose-on-close — it's a separate close-blocking gate (analogous to the existing blaze-issues pre-close check) not yet covered in this entry. Corresponding hubits-api-sales endpoints (a SalesPostingController + the CloseRegister financial-posting gate check) were not in this diff and should be confirmed on the prod API branch before writing a dedicated subsection. (auto-applied by the truth pipeline; adversarially gated)2026-07-06 — hubits-api-sales added session-level audit-trail logging (commits 6f242ad..e31810a): OpenRegister, CloseRegister, CancelCloseRegister, and AdjustRegister now each write an OperationLog entry (TouchCreate/Touch with a descriptive remark, e.g. "Register opened.", "Session closed. ", "Session close cancelled...", "Session adjusted..."), and a new endpoint GET /{sessionId}/history returns the session's full audit timeline (action label, timestamp, actor, remarks, and per-field before/after diffs parsed from OperationLog.ExtraDetails), explicitly modeled on the catalog/finance History tabs. This is additive — none of the existing documented cash-handling logic (variance formula, withdrawal OTP threshold, unconditional PendingClose-on-close, delivery-cash acknowledge-only-flips-flag) changed. No counter.app UI change is present in this diff, so it's unconfirmed whether a session-history UI panel has shipped yet — worth a follow-up check before adding a dedicated section to this entry. (auto-applied by the truth pipeline; adversarially gated)
Note: Playwright verified 2026-05-04 · live E2E (partial) Verified_by:
playwright· Last verified: 2026-05-04 Playwright pass (2026-05-04): Session Details modal confirmed at counter app/salessessions/viewsessiondetails. Tab bar: Overview / Sales Orders / Receipts / Delivery Cash / Cash Withdrawal / Handover (screenshot 174). Cash Withdrawal tab: "Cash Withdrawals" heading, "New Withdrawal" button, empty table with TIME / EMPLOYEE / AMOUNT / REASON / NOTES / ACTIONS columns + "Total Withdrawn (MVR): 0.00" footer (screenshot 175). New Cash Withdrawal form: Current Drawer Balance info bar (MVR 145.80), Withdrawal Amount (required, MVR prefix, quick-fill 100/200/500/1000), Reason dropdown (Petty Cash / Supply Purchase / Delivery Tips / Emergency Expense / Bank Deposit Preparation / Other), Notes optional, Save/Cancel (screenshot 176). Standard E2E account can see and access the form — no visible permission gate. Actions on existing withdrawals not verifiable (no existing withdrawal rows). Expected-balance formula confirmed by column names (Opening/Expected/Counted/Difference). Code access: not available (private repos).
2026-06-04 — codebase: hubits-api-sales@5274891 (SessionsController.cs, CashWithdrawalDtos.cs). REAL CHANGES applied: (1) variance sign convention was backwards — code computes
Difference = Counted − (Opening + Expected), so negative = short / positive = over (SessionsController.cs:1708, get-reconciliation-history:4048); (2) cash withdrawals have an amount-based manager-OTP authorization gate (CashWithdrawalAuthorizationLimit, request-otp/validate-otp endpoints) — the prior "no permission gate" claim was a partial E2E observation under the limit; (3) edit + delete withdrawal endpoints confirmed (open-session only), resolving the prior "unconfirmed" note. Expected formula (Opening + cash sales − refunds − withdrawals) re-verified accurate. UI for OTP prompts + withdrawal Actions column not yet screenshotted.2026-06-14 — TRUTH AUDIT vs prod branches: foodops.counter.app@df086e4 (main), hubits-api-sales@f79eb7b (main). Corrections applied: (1) "Variance does not block closing — closes the session regardless" was WRONG — CloseRegister now routes ALL closes (variance or not) through
PendingClose, requiring a supervisorapprove-closestep before fullyClosed(SessionsController.cs:1811-1821, ApproveCloseRegister@1914). (2) Deleted a fabricated "known jQuery display quirk" (Amount column stuck at 0.00 until jQuery loads) in 2 places — counter.app is React+Vite+TS with no jQuery; Amount = Count × Denomination is computed reactively (OpenRegisterModal.tsx:95,239). (3) Quick-fill withdrawal preset buttons are 50/100/500/1000, not 100/200/500/1000 (AddCashWithdrawalModal.tsx:425) — the 2026-05-04 E2E value is now stale. (4) "Both reason and notes required" corrected — Notes is optional (CashWithdrawalDtos.cs CreateCashWithdrawalRequest has no [Required]; UI placeholder "Optional"). (5) Variance display is the signed Difference (red if negative), not an absolute "short/over" label (ViewReconciliationDetailsModal.tsx:321-327). (6) Removed the non-existent "+ Delivery Cash Acknowledged" Expected term — acknowledge endpoint only flips IsAcknowledged (SessionsController.cs:3483); close Expected reads cash Payments + withdrawals only. (7) Hard-coded tab positions ("tab 5 of 6" / "tab 4 of 6") removed — Session Details tabs are conditional (up to 9: SessionDetailsModal.tsx:302-311). (8) "All tabs must be reviewed before submitting" softened — not enforced. FLAGGED UNVERIFIED: opening-balance admin-correction path (no endpoint found in SessionsController); whether close-path cash sum nets refunds via negative Payment Amount (filters only by Cash + Successful, not PaymentType=Pay); exact acknowledge-handover → cash-Payment-in-Expected chain.2026-06-29 — LAUNCH RE-VERIFY (new-stack only). Re-pinned to foodops.counter.app@32669a9, hubits-api-sales@19190b9. Re-confirmed UNCHANGED: opening-float denomination flow, withdrawal OTP/authorization-limit gate ("Manager authorization required for amounts over…", drawer-balance cap "Withdrawal amount exceeds available cash in drawer"), edit/delete-only-while-open guards, delivery-cash acknowledge only flips IsAcknowledged, close-path Expected = cash payments − withdrawals (no refund term), every-close→PendingClose. CORRECTED: the withdrawal-edit method was renamed
UpdateCashWithdrawal→EditCashWithdrawal(route POST cash-withdrawals/{id}); repinned the closed-session-guard cite to L3142-3144/L3265-3267 (messages unchanged). NEW_INFO: a new Issues tab appears in Session Details when the org enablesEnforceUniqueBlazeNumber(lists invoices with missing/duplicate bank-transfer "blaze" references) and tapping Close Register runs a pre-close blaze check — added to the tab-bar list. SessionsController.cs grew ~210 lines so other inline L-numbers in this entry read low by ~50-210 lines; symbol-anchored cites remain valid.2026-06-15 — ADVERSARIAL SECOND PASS (re-verified the 2026-06-14 corrections against prod HEADs: foodops.counter.app@df086e4=main HEAD, hubits-api-sales main HEAD 2458018; f79eb7b pin still a valid commit on main). 7 corrections re-read and UPHELD against code: PendingClose-on-every-close (CloseRegister@1821 sets status unconditionally), jQuery-quirk deletion (no jquery in package.json; reactive Amount), 50/100/500/1000 presets (AddCashWithdrawalModal@425), Notes-optional (no [Required] in DTO; UI placeholder), signed-Difference-red-if-negative (ViewReconciliationDetailsModal@321-327), acknowledge-only-flips-IsAcknowledged (AcknowledgeCashHandovers@3483), 9 conditional tabs (SessionDetailsModal@302-311). 3 corrections TIGHTENED: (a) edit/delete withdrawal error string was misattributed — actual messages are "Cannot edit/delete withdrawal from a closed session", not Create's "Session not found or is closed"; (b) "open with zero balance shortcut" overstated — it is an error-fallback button shown ONLY when currency config fails to load (rendered inside the currenciesError branch @165-174), not a general form shortcut; the submit-together evidence is handleConfirm@111-123, not @179-185; (c) Current-Drawer-Balance citation repointed 480→395-400. 1 flag PARTIALLY RESOLVED + retightened: close-path Expected confirmed to NOT subtract refunds (payment query @1700-1705 has no PaymentType filter), vs reconciliation-form which does (Pay−Refund@3693,4091); residual unknown narrowed to how refund Payment rows are persisted (negative-Amount vs Refund-type) — flagged honestly. No correction was found to be outright wrong/refuted; the original fabrications (jQuery quirk, abs short/over label, +Delivery-Cash-Acknowledged term, both-fields-required) stay removed.