Foodops
All docs

Counter · Updated 2026-08-24 · ve0a9cf2

Sales Registers

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

Plain-English summary

A register is the named, logical till that a sales session opens against. Think of it as the cash drawer's identity card — it tells Foodops which physical point of sale a cashier is working at and ensures every order, payment, and cash movement during the shift is attached to the right till. It is not a person (that's the cashier) and it is not a location (that's the outlet) — it is the specific POS terminal itself.

When you'd use this

  • Configuring a new outlet — the first thing to add after creating an outlet is at least one register
  • Investigating a discrepancy — "Register 1 shows a variance but Register 2 doesn't" tells you which till to inspect
  • Training a new cashier — explaining why they must open their assigned register, not just any available session
  • Troubleshooting access — if the Counter screen shows no registers to open, someone needs to add one under Settings → Restaurant → Outlets before anyone can start a shift

What a register is (vs outlet vs cashier)

Three concepts that are easy to conflate:

Concept What it is Example
Outlet Physical location — a restaurant branch "Hulhumale Branch"
Register Named till inside an outlet "Register 1", "Bar Till"
Cashier The person operating the till for one session "Aminath"

An outlet can have multiple registers (one per physical till). Each register can have at most one open session at a time. Many cashiers can work the same register over its lifetime, each opening and closing their own session.

Per-outlet register configuration

Registers are configured per outlet in the FoodOps admin app under Settings → Restaurant → Outlets — each outlet card has a Registers section where you add, edit, and delete its registers [code: foodops-admin-app/apps/admin/src/components/settings/restaurant/sections/RegistersSection.tsx]. The settings route is /settings/restaurant/outlets [code: foodops-admin-app/apps/admin/src/App.tsx]; the API base is /settings/restaurant/registers [code: foodops-admin-app/apps/admin/src/services/salesRegisterSettingsService.ts:BASE].

Adding or editing a register opens a dialog (there is no standalone /settings/registers/edit/<id> route). The dialog exposes Name, Description, operating hours (Opens / Closes), a Logo, and three auto-print toggles under an Auto-print heading — "Print kitchen order tickets automatically", "Print receipts automatically", and "Print receipt on new order" [code: foodops-admin-app/apps/admin/src/components/settings/restaurant/RegisterFormDialog.tsx]. The register entity additionally carries document-template assignments (invoice / credit-note / payment-receipt templates, UseReceiptPrinterTemplate) and a CashWithdrawalAuthorizationLimit (default 200 — the amount a cashier can withdraw before supervisor authorization is required) [code: haafai.domain/haafai.domain/Haafai.Models/Entities/SalesManagement/Registers/SalesRegister.cs@staging]. The settings API does accept these fields on create/update [code: hubits-api-sales/Hubits.Sales.Api/Controllers/RestaurantSettingsController.cs:CreateRegister/UpdateRegister + registers/for-edit returns template lookups].

All Registers list — Name, Outlet, Status columns with Register 1 (E2E Test Outlet, Open)

Register 1 detail page — Name, Outlet (E2E Test Outlet), Status (Open), Register Details section with audit info

Each outlet must have at least one register before cashiers can open sessions. The Counter landing screen lives at /counter (the app root / redirects there); for restaurant brands the sidebar item is labelled "Orders" but it still routes to /counter [code: foodops.counter.app/src/App.tsx; foodops.counter.app/src/components/layout/Sidebar.tsx]. It reflects the live state of each register: a register is either shown in the Open Sessions list (a session is active on it) or in the Registers section below (it is closed and available to be opened) [code: foodops.counter.app/src/pages/CounterPage.tsx; hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:GetMySessionsAndRegisters].

If the Registers section shows the placeholder text "All registers are open.", every available register already has an active session — pick one from the Open Sessions list and tap Join to take it over [code: foodops.counter.app/src/pages/CounterPage.tsx]. A closed register in the Registers section is opened with the Open Register button (not "Resume").

One open session at a time per register

A register can only have one open sales session at a time. This is a system-level constraint, not a policy setting — Foodops will not let you open a second session on a register that already has one active. The open-register endpoint runs a compiled IsOpen lookup and rejects with "Register already has an open session" if one exists [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:OpenRegister; hubits-api-sales/Hubits.Sales.Api/Infrastructure/Data/CompiledQueries.cs:GetOpenSessionIdForRegister].

Note (2026-06-03): a register frees up the moment a close is submitted — the session's IsOpen flag is set to false at close-submit even though the session then moves to PendingClose status awaiting supervisor approval [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:CloseRegister (sets IsOpen = false and SalesRegisterSessionStatusId = PendingClose)]. Because the Counter screen's available-registers list and the open-session check both filter on IsOpen [code: hubits-api-sales/Hubits.Sales.Api/Services/Counter/SalesRegisters/SalesRegisterSessionHelper.cs:GetAllOpenSession], a register can be available for a new session while its previous session is still PendingClose in the Sales Sessions list. The register's own open/closed state is unaffected by the new session status. See Session Lifecycle.

This matters in practice because:

  • If a cashier leaves without closing, the register stays in "session open" state. The next cashier must either Join the existing session (taking it over) or wait for the first cashier to close it.
  • Reports and cash reconciliation depend on this guarantee: every transaction in a shift can be traced to exactly one session on exactly one register.
  • Parallel registers are fine — multiple registers in the same outlet can each have their own open session simultaneously. The constraint is one-session-per-register, not one-session-per-outlet.

Cross-device behavior

A session is tied to a register, not to a device. If two devices are logged in to the same outlet and both navigate to the Counter page, they will see the same register list. If cashier A opens Register 1 on device A, cashier B can see that session on device B and join it.

State updates propagate in real time via SignalR: opening or closing a register broadcasts SessionOpened / SessionClosed to the organization's hub group, so every other tablet's Open-Sessions list refreshes without a manual reload [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:OpenRegister/CloseRegister (IHubContext broadcasts to HubGroups.Organization)]. See cross-device-session-sync.md for the lockout + reconnect-revalidation behavior.

POS role gate

Access to the Counter screen (/counter) requires authentication — an unauthenticated user is redirected to /login [code: foodops.counter.app/src/components/authentication/ProtectedRoute.tsx]. A user sees only the registers in the outlets they have been granted access to: the Counter's session/register query scopes both by organization and by the user's ApplicationUserOutlets grants (active outlets only), so a register in an outlet the user isn't assigned to does not appear [code: hubits-api-sales/Hubits.Sales.Api/ViewModels/Counter/Sessions/HomeLinkSalesRegisterSession.cs:Initialize (filters SalesRegisters by ApplicationOrganisationId + outletIds from ApplicationUserOutlets), reached via SessionsController.cs:GetMySessionsAndRegisters]. (For the retail/Salesmade build only, an empty outlet-grant list falls back to all org outlets; the restaurant POS keeps the strict per-outlet filter.)

Key concepts

  • Register — a named logical till within an outlet. The unit against which a sales session is opened. Not a device; any device can operate any register the logged-in user has access to.
  • Outlet — the physical location that owns one or more registers. Configuring an outlet is the prerequisite for adding registers.
  • Session — the open-to-close lifecycle on a single register during one shift. See Sales Sessions for the full session lifecycle.
  • Register state — a register is either open (a session is active) or closed (no session active, ready to be opened). This state is what the Counter landing screen shows.
  • One-session rule — a register cannot have two concurrent open sessions. The next session can only begin once the current one is closed.

Common questions

Q: What is the difference between a register and a session? A: A register is the permanent named till; a session is the temporary shift on that till. A register exists even when no session is open. Over its lifetime, a register accumulates many sessions — one per shift.

Q: Can I rename a register? A: Register names are edited in the admin app under Settings → Restaurant → Outlets (the register's edit dialog) [code: foodops-admin-app/apps/admin/src/components/settings/restaurant/RegisterFormDialog.tsx]. Contact a Manager or Administrator to make the change.

Q: Can two cashiers work the same register at the same time? A: A register can have only one open session at a time, so two cashiers cannot each run their own session on it [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:OpenRegister]. They can both join the same open session from different devices, coordinated via SignalR (see cross-device-session-sync.md). To hand the till to another cashier, use the session handover flow.

Q: Can one cashier open sessions on multiple registers?

A: A cashier can open a session on Register 1, close it, then open a new one on Register 2. The constraint is per-register (one open session each), not per-cashier — the system enforces only "one open session per register" [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:OpenRegister].

Q: Who can see which registers? A: A user sees only the registers in the outlets they have been granted access to. The Counter's register/session list is filtered server-side by organization and by the user's per-outlet access grants (ApplicationUserOutlets) [code: hubits-api-sales/Hubits.Sales.Api/ViewModels/Counter/Sessions/HomeLinkSalesRegisterSession.cs:Initialize, via SessionsController.cs:GetMySessionsAndRegisters]. If a register is missing from the Counter screen, check the user's outlet assignment. See Users, Roles & Permissions.

Q: What happens to a register's historical sessions when the register is deleted? A: Deleting a register is a soft delete — the register row is marked Deleted rather than removed, and deletion is blocked while it has an open session [code: hubits-api-sales/Hubits.Sales.Api/Controllers/RestaurantSettingsController.cs:DeleteRegister (sets RecordStatusId = Deleted)]. Historical sessions remain in the Sales Sessions list for audit purposes, and the register name continues to appear on historical records.

Edge cases and known issues

Cannot reach the Counter screen

  • Symptom: navigating to the Counter screen does not show the register list.
  • Cause: the session is not authenticated — ProtectedRoute redirects unauthenticated users to /login [code: foodops.counter.app/src/components/authentication/ProtectedRoute.tsx]. (Note: the restaurant sidebar labels this destination "Orders", but the route is /counter.)
  • Workaround: log in again; if a user lacks outlet access, an Administrator grants outlet/role access. See Users, Roles & Permissions.

No registers available to open on a newly created outlet

  • Symptom: the Counter screen's Registers section is empty (placeholder "All registers are open.") right after an outlet is created.
  • Cause: outlets are created without any registers by default; registers must be added manually.
  • Workaround: Settings → Restaurant → Outlets → add at least one register before sending cashiers to the Counter.

Register appears open but the cashier has left for the day

  • Symptom: Register 1 appears in "Open Sessions" but the assigned cashier is not working.
  • Cause: the cashier left without closing their session. Nothing auto-closes sessions.
  • Workaround: another authorized user joins the session and closes it [code: hubits-api-sales/Hubits.Sales.Api/Controllers/SessionsController.cs:JoinSession + CloseRegister — both gated only by [ApiKeyOrSessionAuth], no role attribute], or closes it from the Sales Sessions list in the admin app (/sales/register-sessions) [code: foodops-admin-app/apps/admin/src/App.tsx]. See Sales Sessions.

Note: a close lands the session in PendingClose; a separate approval step (approve-close, also no role attribute in this controller) is what moves it to fully Closed.

Related

  • Sales Sessions — the open-to-close lifecycle that runs on a register

  • Cross-device session sync — SignalR lockout coordinating register access

  • Session Handover — transferring a register to a different cashier mid-shift

  • Cash Handling — opening float, withdrawals, and close-count on a register

  • Users, Roles & Permissions — Counter role and outlet-scope access

  • 2026-08-22 — UPSTREAM WATCH ([email protected]): four changes touch this entry's cited surface, none register-level. (1) RestaurantSettingsController's outlet DTO/CreateOutlet/UpdateOutlet gain eight new outlet-level fields — VoidBinApprovalMode, EnableQuickUndo, QuickUndoTimeoutSeconds, QuickUndoDiscard, QuickUndoTableMove, QuickUndoItemMove, VoidReasonMandatory, and EnableQuickSessionSwitch — all outlet-scoped settings, not register fields; no CreateRegister/UpdateRegister/DeleteRegister hunk is shown in this diff, so none of this entry's register-config claims (Name/Description/hours/auto-print/templates/CashWithdrawalAuthorizationLimit) are touched. (2) GetMySessionsAndRegisters now stamps EnableQuickSessionSwitch (read from SalesRegister.Outlet) onto each OpenSessions row — this is the backend source of the enableQuickSessionSwitch flag the 2026-08-21 note found only client-side (unifiedOfflineDB.sessionState); now confirmed server-sourced from the outlet setting rather than a purely local/offline flag. (3) GetSessionCloseSummary and GetSessionCloseSummaryPdf both now attach a DiscardLog (via SessionDiscardLogService.BuildAsync) and a SalesRegisterId to the close-summary DTO — corroborating the 2026-08-20 Discard Log note by wiring it into the close-of-shift summary/PDF and giving the summary a direct register reference; still session-level reporting, not a register-entity change. (4) CloseRegister's void-bin close gate: the guarding comment now reads 'approved/rejected/acknowledged' (previously 'approved/rejected') and the returned error message changes from 'Review and clear the void bin before closing the register.' to 'Review and clear the discard bin before closing the register.' — the visible diff only touches the comment and message text, not the unreviewedVoids query body itself, so a third 'acknowledged' review state is asserted by the comment but not independently confirmed here. This is the backend confirmation of the void→discard-bin terminology shift the 2026-08-21 note observed only on the frontend gate-message regex and toast copy — the rename is now evidenced end-to-end at the source. None of these four changes touch register identity, the one-session-per-register rule, register config fields, soft-delete, cross-device SignalR sync, or outlet-scoped register visibility, so no claim in this entry is contradicted. The outlet-level void-bin/quick-undo settings and the close-summary DiscardLog wiring are better documented in an outlet-settings entry and sales-sessions.md/cash-handling.md respectively; flagged here only because RestaurantSettingsController.cs and SessionsController.cs are cited source files for this entry. No body edit needed. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-21 — UPSTREAM WATCH ([email protected]): two genuinely new changes on the cited session surface, neither register-level. (1) SessionDetailsModal.tsx gains a 'Quick Session Switch' path — gated per-session by a new enableQuickSessionSwitch flag read from unifiedOfflineDB.sessionState — that renders a QuickSwitchDropdown listing the org's other open sessions (register name, outlet name, reference, lock state) and, on selection, calls joinSession(id) then navigates to /sessions/{id}/pos, letting a cashier jump straight from one session's details to another open session/register without returning to the Counter screen first. (2) isVoidBinGate's message match widens from /void bin/i to /(void|discard) bin/i, and the close-blocked toast copy changes from 'Review and clear the void bin...' to 'Review and clear the discard bin...' — the close-time gate catching up to the 'void bin' → 'Discard Log' rename corroborated by the 2026-08-20 note's DiscardLogTab/DiscardOrderLogModal addition, confirming the terminology shift is now consistent end-to-end between the gate message and the review surface. A third hunk (SessionOrdersTab.tsx wiring hasCreatePermission from sessionApiService.getOrderButtonConfig) reappears from the already-captured 2026-08-12 note — same change, no new action. None of these touch register identity, the one-session-per-register rule, register config fields, soft-delete, cross-device SignalR sync, or outlet-scoped register visibility, so no claim in this entry is contradicted. The Quick Session Switch feature and the void→discard bin renaming are better documented in sales-sessions.md (or a session-handover/discard-log entry) once the enableQuickSessionSwitch rollout/permission gate is corroborated in hubits-api-sales source; flagged here only because SessionDetailsModal.tsx and SessionOrdersTab.tsx are cited source paths for this entry. SessionSalesSummaryCard.tsx's changes in this diff are a pure CSS/markup refactor (Card → div, grid-gap divider styling) with no behavior change. No body edit needed here. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-20 — UPSTREAM WATCH ([email protected]): a new session-level 'Discard Log' surface lands — DiscardLogTab.tsx + DiscardOrderLogModal.tsx (both net-new, with full unit-test coverage). The tab lists an outlet's per-session void/discard and rejection requests (order id, date/time, order type, reason, requested-by, reviewed-by, total, and a status pill of Approved/Rejected/Approval Pending) with overview stat cards (order count, total amount); clicking a row opens a modal merging the order's OperationLog audit trail with synthesized 'Discard Requested'/'Discard Approved|Rejected' timeline events. This corroborates the void-bin close-gate flagged in the 2026-08-04 note (cashiers now have a dedicated place to review/clear void-bin entries) but is session/order-level reporting, not a register-entity change. Two smaller changes in the same range are also session-level, not register-level: SessionOrdersTab now wires the order-details panel's hasCreatePermission to a real per-session lookup (sessionApiService.getOrderButtonConfig) instead of a hardcoded false, and sessionSlice's offline-hydrate/detect paths persist a new enforceUniqueBlazeNumber flag alongside the already-tracked posDefaultViewMode/softEnforcePaymentReference so it survives a hard refresh. None of these touch register identity, the one-session-per-register rule, register config fields, soft-delete, cross-device SignalR sync, or outlet-scoped register visibility — no claim in this entry is contradicted. The Discard Log tab is better documented in sales-sessions.md (or a dedicated void-bin/discard-log entry) once the backend endpoint(s) behind getSessionDiscardLog are corroborated in hubits-api-sales source; flagged here only because src/components/session/** is a cited source path for this entry. No body edit needed here. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-12 — UPSTREAM WATCH ([email protected]): three changes touch this entry's cited surface, none register-level. (1) SessionOrdersTab.tsx (src/components/session/**, a cited source path) now fetches sessionApiService.getOrderButtonConfig(sessionId) and passes the real hasCreatePermission value to OrderDetailsCard instead of a hardcoded false — per the commit subject ('fix: use real create permission in session order details') this fixes a bug where opening an order from the Session Orders tab always suppressed create actions regardless of the user's actual permission. (2) SessionDetailsModal.tsx (same cited path) passes a new onRefresh callback (fetchSessionDetails(true)) into the orders tab so actions taken in the reopened order details refresh the parent session snapshot instead of leaving stale row state. (3) sessionSlice.ts (a cited source file) extends the existing offline-hard-refresh persistence path to also carry enforceUniqueBlazeNumber, mirroring the softEnforcePaymentReference persistence already noted in the 2026-08-11 verification-history entry — this is the frontend catching up to the EnforceUniqueBlazeNumberForSessions flag hubits-api-sales started returning per that same note. None of these three changes touch register identity, the one-session-per-register rule, register config fields, soft-delete, cross-device SignalR sync, the Counter route/sidebar, or outlet-scoped register visibility — they are session-order-permission and session-persistence plumbing, not a register-entity change. The other commits in this range (hiding Take Out/Delivery buttons on dine-in, org-timezone fixes for manual receipt/KOT printing, and anchoring the Running badge's start instant in UTC) don't touch any file cited by this entry. Better documented in sales-sessions.md; flagged here only because SessionDetailsModal.tsx, SessionOrdersTab.tsx, and sessionSlice.ts are cited source paths for this entry. No body edit needed. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-11 — UPSTREAM WATCH ([email protected]): GetMySessionsAndRegisters — a cited source file for this entry — now also reads the org-level 'Enforce Unique Blaze Number' setting and stamps it onto every OpenSessions row as EnforceUniqueBlazeNumberForSessions. Per its own inline comment, this setting was already returned on the session-DETAILS payload (Issues tab / close gate) but is now surfaced at the Open-Sessions/payment surface too, gating whether a Transfer Receipt whose reference was already used in the session raises a warning; OFF (the default, absent-row → false) leaves blaze-uniqueness detection fully dormant. The rest of this diff range (OpenedTime UTC ISO-8601 serialization, PosDefaultViewMode/SoftEnforcePaymentReference stamping, outlet EnableVoidBin pass-through, and the dual org+outlet reference-number preview rework) re-surfaces changes already captured in the 2026-08-02/08-05 verification-history notes. This is session/payment-level plumbing, not a register-entity change — it does not touch register identity, the one-session-per-register rule, register config fields, soft-delete, cross-device SignalR sync, or outlet-scoped register visibility, so no claim in this entry is contradicted. Better documented in sales-sessions.md (or a payments/session-settings entry) once a frontend surfaces the warning; flagged here only because SessionsController.cs is a cited source file for this entry. No body edit needed. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-08 — UPSTREAM WATCH ([email protected]): App.tsx — a cited source file for this entry's /sales/register-sessions route — switches its route-chunk imports from bare React.lazy(...) to a new lazyWithReload(...) helper across the whole route table (Dashboard/Finance/Salesmade/Assistant pages confirmed by visible +/- hunks; the LoginPage-through-SalesReturnDetailsPage block, which includes RegisterSessionsPage and RegisterSessionDetailsPage, is truncated in this diff so their individual reassignment isn't directly visible, but they sit inside the same rewritten block). Per its own inline comment, this is a deploy-resilience fix: after a deploy replaces the container, a stale tab's request for an old hashed chunk 404s, and lazyWithReload turns that into one self-healing reload instead of an uncaught rejection that blanks the app. This is global chunk-loading infrastructure, not a route path, permission, or register/session behavior change — the /sales/register-sessions route string and RegisterSessionsPage's actual contents are unaffected as far as this diff shows (its own hunk is PATCH-UNAVAILABLE, so no visibility into that file's body). None of the commit subjects in this range (Finance Projects page + project dropdowns, customer-invoice pagination fix, secondary-fetch caching, mobile top-bar/item-detail fixes, payment date fix) touch registers or sessions. No claim in this entry is contradicted; no body edit needed. Flagged only because App.tsx is a cited source file for this entry's admin routes. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-08 — UPSTREAM WATCH ([email protected]): this wider diff mostly re-surfaces changes already captured in the 2026-08-01/08-02/08-05 verification-history notes (close-summary DI wiring, PosDefaultViewMode/SoftEnforcePaymentReference session stamping, the OpenedTime UTC ISO-8601 fix, EnableVoidBin pass-through, and the org+outlet dual-scope reference-number preview) — none of that is new here and none touches register identity, config, or lifecycle. One genuinely new hunk: OpenRegister's cash-balance seeding now explicitly extracts openingAmount and (per a truncated inline comment) seeds the session balance's Expected amount to the opening float — "with no sales and no withdrawals yet, what should be in the drawer IS the float." This is session/cash-reconciliation plumbing feeding the Expected-amount figures already covered by the 2026-07-30 'blind cash counting' watch note, not a register-entity change — it does not touch OpenRegister's IsOpen/one-session-per-register rejection, CloseRegister's PendingClose transition, or any claim in this entry. Better documented in cash-handling.md (how the opening float becomes the initial Expected baseline) than here; flagged only because SessionsController.cs is a cited source file for this entry. No body edit needed. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-05 — UPSTREAM WATCH ([email protected]): four changes touch this entry's cited surface, none contradicting a register-level claim. (1) GetMySessionsAndRegisters' OpenedTime serialization changes from a bare 'yyyy-MM-dd HH:mm:ss' string to an explicit UTC ISO-8601 round-trip string (DateTime.SpecifyKind(..., Utc).ToString("O")) — per the inline comment this fixes a real display bug where a browser parsed the naive string as local time and showed a just-opened register's clock hours off (e.g. 03:02 instead of 08:02 in UTC+5). This is a bugfix to how OpenSessions rows render, not a change to register identity, the one-session-per-register rule, or scoping — no claim in this entry references a specific OpenedTime format, so nothing here needs correcting, but it's worth noting in sales-sessions.md if OpenedTime formatting is ever documented there. (2) PosDefaultViewMode/SoftEnforcePaymentReference (already flagged 2026-08-02) and (3) the close-summary/PDF endpoints' backing DI wiring (DaySummaryReportService, IHtmlToPdfService — already flagged 2026-08-01) are now fully visible in this wider diff range but add no new register-relevant detail. (4) RestaurantSettingsController's EnableVoidBin is now shown with its actual (pass-through) implementation on CreateOutlet/UpdateOutlet/for-edit, and the outlet reference-number preview rework (dual org+outlet scope peek via a new OrgPreview helper) is now fully visible — both were anticipated with 'no behavior body visible' in the 2026-08-02 note and are confirmed here as outlet-level settings/reference-numbering only, not touching CreateRegister/UpdateRegister/DeleteRegister or any register config field. No body edit needed here; flagged only because SessionsController.cs and RestaurantSettingsController.cs are cited source files for this entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-04 — UPSTREAM WATCH ([email protected]): closing a register gains a second close-time gate alongside the existing financial-postings gate (2026-07-08/07-09 notes) — a 'void bin' gate. SessionDetailsModal's close-register handler now matches a /void bin/i message pattern on a failed close response and, instead of the generic error toast, closes both the close-register and session-details modals then shows a toast with a 'Review' action that stashes the session id (foodops_open_discard) in sessionStorage and navigates to /sessions/{sessionId}/dinein so the cashier can review/clear the void bin before retrying the close. Per the accompanying unit test (SessionDetailsModal.forceClose.test.tsx), this gate applies even to blind-count users — it is NOT bypassed by the blind force-close path, unlike a generic close failure. This is a new close-time failure mode (frontend-only diff; no accompanying hubits-api-sales change confirms the backend gate/message in this diff, so the server-side 'void bin' rule itself is not yet evidenced here). Does not contradict any existing claim in this entry — the one-session-per-register rule and the close-submit → PendingClose flow both describe the success path only — but this entry's 'Register appears open but the cashier has left' edge case (another user joins and closes) may now be blocked by this gate until the void bin is reviewed; worth a short new edge-case/known-issue note once the backend gate is corroborated in hubits-api-sales source. The unrelated SessionPaymentsTab.tsx change (linking Transaction Ref out to the admin payment detail page when transactionId is present) and sessionSlice.ts change (persisting posDefaultViewMode/softEnforcePaymentReference through an offline hard-refresh, extending settings already noted 2026-08-01/08-02) are session-UI/persistence-only and don't touch register identity, config, or lifecycle. Sidebar.tsx's new AdminAppRedirectDialog confirmation only applies to external (admin-app-bound) sidebar rows and does not change the Counter/'Orders' row's route to /counter (unaffected, consistent with the 2026-08-03 note). No body edit needed here; flagged only because SessionDetailsModal.tsx, sessionSlice.ts, Sidebar.tsx, and SessionPaymentsTab.tsx (src/components/session/**) are cited source paths for this entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-03 — UPSTREAM WATCH ([email protected]): three changes touch this entry's cited surface but none contradict a register-level claim. (1) SessionPaymentsTab.tsx (src/components/session/**) now links each payment's Transaction Ref out to the admin payment detail page (admin.../sales/payments/{transactionId}) when payment.transactionId is present, opening in a new tab (unit-tested); the ref stays plain text when transactionId is absent. This is a session-payments-detail UI feature, not a register-entity change. (2) sessionSlice.ts (a cited source file) extends the existing snapshot-rehydrate/detect-session path to also persist and restore posDefaultViewMode and softEnforcePaymentReference — the two org-level POS settings already surfaced by the 2026-08-02 verification-history entry — so they survive an offline hard-refresh; this is persistence plumbing for an existing setting, not a new one. (3) Sidebar.tsx (a cited source file) adds a confirm-before-navigate dialog (AdminAppRedirectDialog, gated by a new shouldConfirmAdminNav() opt-out preference) for sidebar rows marked external (links out to the admin app), replacing the prior unconditional window.open; a modified click (ctrl/cmd/shift/alt) now falls through to native anchor behavior for all rows instead of being specially intercepted only for external ones. This only changes rows that navigate OUT to the admin app — the diff does not touch the Counter/'Orders' row's route to /counter, so the entry's 'sidebar item is labelled Orders but still routes to /counter' claim is unaffected. None of the three changes touch register identity, the one-session-per-register rule, register config fields, soft-delete, cross-device SignalR sync, or outlet-scoped register visibility. The payment-detail deep link is better documented in sales-sessions.md (Payments tab) than here; the posDefaultViewMode/softEnforcePaymentReference persistence is implementation detail of settings already flagged for sales-sessions.md/counter-settings. No body edit needed here. Flagged only because SessionPaymentsTab.tsx, sessionSlice.ts, and Sidebar.tsx are cited source paths for this entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-02 — UPSTREAM WATCH ([email protected]): SessionsController's GetMySessionsAndRegisters now stamps two new org-level settings onto every OpenSessions row — PosDefaultViewMode (via PosViewSettings.ReadDefaultViewMode, the Outlets General 'Default to Tile View' toggle, defaults to 'compact' when the row is absent) and SoftEnforcePaymentReference (via PaymentReferenceSettings.IsSoftEnforcedAsync, the Outlets General 'Soft Enforce Payment Reference Number' toggle, which when ON requires a reference for Transfer payments on the salesmade counter; default OFF/not-enforced). Separately, RestaurantSettingsController's outlet DTO/CreateOutlet/UpdateOutlet gain a pass-through EnableVoidBin field (no behavior body visible in this diff), and the outlet reference-number-preview logic (BuildOutletReferenceConfigDto path) is reworked to peek BOTH the org-wide and the outlet's own sequence scope so the settings UI can live-preview either numbering scheme without a save round-trip, instead of only previewing the currently-saved scope. All of this is session-level (org-wide POS/payment defaults) or outlet-level (void bin, reference numbering) — none of it touches register identity, the one-session-per-register rule, register config fields (Name/Description/hours/auto-print/templates/CashWithdrawalAuthorizationLimit), soft-delete, cross-device SignalR sync, or the outlet-scoped register-visibility filter, so nothing in this entry is contradicted. The GetSessionCloseSummary/GetSessionCloseSummaryPdf endpoints also visible in this diff range were already captured by the 2026-08-01 verification-history entry — no new action there. Flagged here only because SessionsController.cs and RestaurantSettingsController.cs are cited source files for this entry; PosDefaultViewMode/SoftEnforcePaymentReference are better documented in sales-sessions.md or a counter-settings entry if/when they surface in the Counter UI. No body edit needed here. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-01 — UPSTREAM WATCH ([email protected]): SessionsController — a cited source file for this entry — gains two new read-only endpoints that back the close-summary print/PDF plumbing previously flagged as frontend-only in the 2026-07-31 watch note: GET {sessionId}/close-summary (returns a SessionCloseSummaryDto built via DaySummaryReportService.GenerateReport with the session's own window, so it reconciles with the day-summary report) and GET {sessionId}/close-summary/pdf (renders the same DTO to HTML and converts it to PDF via a new IHtmlToPdfService, online-only, with Content-Disposition toggled inline/attachment by ?download=true). Per the method's own doc comment this feeds the 'Session Summary' thermal receipt UI already wired client-side (sessionApiService.getCloseSummary/getCloseSummaryPdf) and leaves closed-only gating as a UI concern. This is session-level close-of-shift reporting, not a register-entity change — it does not touch register identity, the one-session-per-register rule, register config fields, soft-delete, cross-device SignalR sync, or outlet-scoping, so nothing in this entry is contradicted. Better documented in sales-sessions.md or cash-handling.md as a 'session close summary (thermal print / PDF)' capability now confirmed backend-complete; flagged here only because SessionsController.cs is a cited source file for this entry. No body edit needed here. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-31 — UPSTREAM WATCH ([email protected]): sessionApiService gains a new close-summary print/PDF path — getCloseSummary(sessionId, forceRefresh) (GET /sessions/{id}/close-summary, offline-first like getSessionDetails: tries cachedGet, falls back to a direct network call, then to a bootstrapCache.loadSessionCloseSummary offline fallback, all persisted via bootstrapCache.saveSessionCloseSummary), a synchronous getCachedCloseSummary(sessionId) reader, and getCloseSummaryPdf(sessionId) (GET /sessions/{id}/close-summary/pdf as a Blob, online-only per its own comment) feeding a server-rendered PDF. Per the commit subjects this is 'session close-summary print plumbing — types, offline cache, broker mapper + submitSessionSummary' for a thermal 'Session Summary' print at close. This is session-level close-of-shift functionality, not a register-entity change — it does not touch register identity, the one-session-per-register rule, register config fields, soft-delete, cross-device SignalR sync, or outlet-scoping, so nothing in this entry is contradicted. Flagged here only because src/services/session/** is a cited source path for this entry; better documented in sales-sessions.md or cash-handling.md as a new 'session close summary (thermal print / PDF)' capability once the UI trigger (button/modal) is located in a follow-up diff. No body edit needed here. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-30 — UPSTREAM WATCH ([email protected]): a new 'blind cash counting' feature gates the Expected Amount / Difference / Variance figures across the session UI (CashReconciliationModal, CloseRegisterModal, SessionBalancesTab, SessionOverviewTab, SessionReconciliationTab, SessionHandoverTab, SessionOrdersTab's Total/Average cards) behind a new canViewExpectedCash/canSeeExpectedForSession permission check — users without the view-expected right no longer see Expected/Difference figures while a session is open (per inline comment, 'so staff count blind'), and CloseRegisterModal's submit button is relabeled 'Close Register' → 'Submit Count'. When a blind-count close produces a variance, SessionDetailsModal now auto-commits the close via a new approveBlindClose() path (calls approveClose then re-fetches session details) instead of offering the sighted recount flow, and shows a new read-only VarianceSummaryModal. This is a session/cash-reconciliation UX change, not a register-entity change — it does not contradict any claim in this entry (register identity, one-session-per-register, register config fields, soft-delete, cross-device SignalR sync, and outlet-scoping are all untouched). Worth documenting in sales-sessions.md and/or cash-handling.md since it changes what cashiers see at close-of-shift; flagged here only because src/components/session/** is a cited source path for this entry. No body edit needed here. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-18 — UPSTREAM WATCH ([email protected]): RestaurantSettingsController — the source file this entry cites for CreateRegister/UpdateRegister/DeleteRegister — gained a class-level [ApiKeyOrSessionAuth] attribute that was NOT present before this commit; the commit subject confirms it closes a gap where 'previously-anonymous mutating endpoints (address/settings/onboarding/layout/org-layout)' could be reached without authentication. This diff's visible hunks only touch Floor/Kitchen/Table CRUD (adding the same auth gate, an org-scoped IDOR fix replacing FindAsync(id) with FirstOrDefaultAsync(x => x.Id == id && x.ApplicationOrganisationId == CallContext.ApplicationOrganisationId), and new PlanLimitHelper plan-tier caps on Floor/Kitchen creation) — no CreateRegister/UpdateRegister/DeleteRegister method body is shown changed, so none of this entry's register-specific claims (soft-delete on DeleteRegister, template/withdrawal-limit fields accepted on create/update) are contradicted. But because the auth attribute is applied at the class level, it now also covers the register endpoints in this same controller, meaning register create/update/delete previously had no auth gate and now require [ApiKeyOrSessionAuth] — worth a short security-relevant note (not a body correction) once a register-specific hunk or a targeted read of CreateRegister/UpdateRegister/DeleteRegister confirms no register-specific behavior also changed. Note also: the new PlanLimitHelper.CheckFloorLimitAsync/CheckKitchenLimitAsync gates are evidenced only for floors/kitchens in this diff — this does NOT confirm (or rule out) whether an equivalent backend CheckRegisterLimitAsync exists to back the frontend register plan-limit gate already flagged in the 2026-07-17 verification-history entry; that remains unconfirmed pending a diff that touches CreateRegister directly. Unrelated to registers: SessionsController changes in this range (new OutletId field on the register DTO in GetMySessionsAndRegisters, OriginalCurrency-aware payment amount/currency display in GetSessionDetails, and swapping PrintHub.GetConnectedPrintersForOrgStatic for an injected IPrinterConnectionRegistry in GetPrinterStatus) are additive/internal and don't touch OpenRegister, CloseRegister, or JoinSession, so none of this entry's session-lifecycle or role-gate claims are affected. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-17 — UPSTREAM WATCH ([email protected]): RegistersSection.tsx adds a subscription plan-limit gate on register creation. openCreate() now runs counterLimitState(items.length, subscriptionEnabled, planLimits) and, once the outlet is at its plan's register cap, blocks the create action (toast error), disables the 'Add register' button, and shows an inline amber banner reading 'Your plan allows a maximum of N registers per outlet. Upgrade your plan to add more.' This does not contradict this entry's claim that an outlet can have multiple registers — it adds a plan-tier ceiling on that count, which is new user-facing behavior not yet documented here. Worth a short note under 'Per-outlet register configuration' once the exact plan-tier limits are confirmed (not visible in this diff). The rest of this commit range (App.tsx routes for ProductFormPage/CreditCyclePage/BillingSettingsPage/StockReadinessSettingsPage; RegisterSessionsPage.tsx patch unavailable) is unrelated to registers/sessions and needs no action here. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-16 — UPSTREAM WATCH ([email protected]): SessionDetector.tsx adds a durable temp-session-id → real-id resolution path for the open-register flow. Per its own inline comments, opening a register queues '/sessions/open-register' and navigates straight to /sessions/temp_x/pos so the cashier can start ringing before the queued call syncs; when it does sync, SyncService re-keys the local session snapshot to the server id and fires a one-shot 'session-id-resolved' event, but that event could go unheard (queue drained during open-register's pre-reload wait, mid-navigation, or before POSPage mounted), causing detectCurrentSession to find the temp id in neither /sessions/my nor the snapshot and bounce the cashier back to /counter — forcing them to rejoin the register they had just opened. The fix adds resolveToServerId(), a durable idMappings lookup re-checked on every mount, plus a resolvedTempIdsRef guard against a same-render URL/state race. This is a bug fix to the register-open UX, not a new documented mechanic — it does not contradict this entry's 'Open Register'/'Join' flow description, but is worth a Known-Issues line once corroborated against the rest of the hook (only visible through ~line 220 in this diff). Also in this diff: SessionDetailsModal.tsx now includes '/sessions/history' in its post-close/cancel-close clearCacheByEndpoints call so the Counter's Sessions list stops serving stale pre-close (PendingClose) rows from a 2-min cache — this reinforces, not contradicts, the entry's documented close-submit → PendingClose transition. Sidebar.tsx's currentOrganization/availableOrganizations change is an org-switcher rendering fix (shows 'Select organisation' when the last-logged-in org is disabled/non-member), unrelated to registers. No body edit needed; the SessionDetector fix is a candidate for a future 'temp session id' known-issue note once the rest of the hook is read. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-10 — UPSTREAM WATCH ([email protected]): Sidebar.tsx replaced the Counter/Orders/Deliveries/Takeouts/Dine-In <button onClick> rows with real <a href> elements (native right-click/ctrl-click/middle-click 'open in new tab' support), backed by a new resolveItemHref() that — per its own comment — mirrors the pre-existing handleMenuClick branching. That function reveals the sidebar's 'Orders'/'Counter' entry does not unconditionally route to /counter: when a currentSession is already active it instead deep-links to /sessions/{currentSession.id}/pos (or /dinein, or /pos?mode=delivery|takeout), only falling back to /counter when no session is open. This is presented as mirroring existing click behavior, not a new routing change, so it does not contradict this entry's claim that the Counter landing screen (the register-picker) lives at /counter — that remains true precisely for the no-active-session case this entry documents. But the entry's flat phrasing ('the sidebar item is labelled Orders but it still routes to /counter') is incomplete and worth qualifying with the active-session caveat once confirmed against handleItemNavigate (not in this diff). Also in this diff but not register-relevant: a Sidebar.tsx fallback-icon rename ('hgi-shipping-center' → 'airplay', cosmetic) and SessionDetector.tsx extending its self-heal enrichment (outletId) to also backfill isQuoteModeEnabled from /sessions/my, with a new guard skipping temp_-id sessions to avoid an infinite detect loop — session-hydration plumbing, unrelated to register open/close/config mechanics documented here. The FailedPostingsDialog/salesPostingService/sessionApiService changes in this same diff range were already captured by the 2026-07-08 and 2026-07-09 verification-history entries; no new action needed there. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-10 — UPSTREAM WATCH ([email protected]): RegisterFormDialog was redesigned ('implement redesigned form with logo upload and improved structure') with new UI primitives (Field/SectionHeading/CheckCard/ImageField) and a logo-upload flow. In parallel, salesRegisterSettingsService.buildFormData now unconditionally sends RestaurantFloorId, AssignedUserId, CashWithdrawalAuthorizationLimit, and RequireCoverSelection on every save, with an explicit comment that these 'must always ride the payload or an edit wipes them' — floor/user are cleared when empty (an 'unassigned' UI state) and the limit is 'always sent (non-nullable)'. RegistersSection.tsx also now fetches floor/template lookups via getEditForm() to resolve names in an expandable per-register detail view. Taken together this is strong (but not full-JSX-confirmed) evidence that CashWithdrawalAuthorizationLimit — previously flagged in this entry as 'still unconfirmed' whether it's surfaced in the dialog (see the 2026-07-04 kb-claim update, which only confirmed template fields) — is now editable in the redesigned dialog, alongside two entirely new concepts not yet documented anywhere in this entry: register-to-floor assignment and register-to-user (cashier) assignment. Does not contradict any currently-documented dialog field (Name/Description/hours/Logo/auto-print all still presumed present) — this is additive. Before editing the main body, read the full RegisterFormDialog.tsx JSX (only partially visible in this diff) to confirm exact field labels/placement, then update the 'dialog exposes...' paragraph and resolve the withdrawal-limit-not-surfaced kb-claim; also consider documenting 'floor' and 'assigned user' as new register concepts. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-09 — UPSTREAM WATCH follow-up ([email protected], supersedes/extends the 2026-07-08 note): the financial-postings close-gate is now fully wired on the frontend, not just implied. sessionApiService.closeRegister explicitly translates a structured HTTP 400 {success:false,message} close-gate response into a normal {success:false,message} result (network/timeout errors with no structured body still throw), and SessionDetailsModal matches gate messages via /financial posting|still posting/i to open a new FailedPostingsDialog. That dialog (backed by salesPostingService) lists an organisation's dead-lettered postings filtered to the current session (GET /v1/sales/posting/dead-letter, client-side filtered by registerSessionId) and lets the cashier retry one or all (POST /v1/sales/posting/{id}/retry) before re-attempting the close. This is confirmed by a full unit-test suite on both the client (sessionApiService.test.ts, salesPostingService.test.ts) and the modal wiring — a materially stronger evidence bar than the 07-08 watch, though the underlying backend gate/endpoints themselves are still not evidenced by any hubits-api-sales diff in this repo. Does not contradict any existing claim in this entry (the one-session-per-register rule and the close-submit → PendingClose note both describe the success path only). Worth adding as a new edge-case/known-issue in this entry or in cash-handling.md — 'register close can be blocked by unposted financial transactions; use the Failed Postings dialog to retry' — once the backend gate is separately confirmed in hubits-api-sales source. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-08 — UPSTREAM WATCH ([email protected]): closing a register can now be blocked by a new financial-postings gate — if any sales in the session failed async financial posting (invoice/payments/journals/stock) after exhausting retries (dead-lettered) or are still mid-posting, closeRegister returns {success:false, message} (HTTP 400) instead of closing, and SessionDetailsModal detects this via a financial posting|still posting message match and opens a new FailedPostingsDialog (backed by salesPostingService, hitting /v1/sales/posting/dead-letter and /v1/sales/posting/{id}/retry) letting the cashier requeue the stuck postings and then retry the close. This is a new close-time failure mode/UI (frontend-only diff; the underlying backend gate is implied pre-existing per the accompanying code comments but not evidenced in this diff since no hubits-api-sales change is included). Does not contradict any existing claim about the one-session-per-register rule or the close-submit → PendingClose flow, which only describe the success path. Worth a short new edge-case note in this entry (or in sales-sessions.md / cash-handling.md) once the backend gate is confirmed in hubits-api-sales source. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-07 — UPSTREAM WATCH ([email protected]): SessionsController now writes an operation-log audit trail on session open/close/cancel-close/adjust (TouchCreate/Touch calls with messages like "Register opened.", "Session closed.", "Session close cancelled.", "Session adjusted.") and adds a new GET {sessionId}/history endpoint returning the full audit timeline (opened/closed/cancelled/adjusted/approved/times-edited entries, newest first, with per-field diffs parsed from OperationLog.ExtraDetails) — per comment, this mirrors the catalog/finance History tabs. This is session-level audit infrastructure, not a register-level behavior change, so no existing claim in this entry is contradicted. No accompanying foodops-admin-app or foodops.counter.app diff surfaces this in a UI yet. Better documented in sales-sessions.md or session-lifecycle.md once a frontend consumes it; flagging here only because SessionsController.cs is a cited source file for this entry. (auto-applied by the truth pipeline; adversarially gated)

Warning: Unverified — status: draft Verified_by: claude-code (internal-docs read, 2026-05-03) · Code access: not available (private repos) Claims derive from internal-docs/foodops/counter/registers-list.md (verified 2026-04-23), internal-docs/foodops/counter/session-details.md (verified 2026-04-23), internal-docs/foodops/counter/join-session.md (verified 2026-04-23), and FoodOps/sales-sessions.md. UI labels in <!-- UI-TODO --> blocks are not confirmed from a verified screenshot and must be checked against the live app on next auth.json refresh.

  • 2026-06-03 — codebase: VERIFIED-NO-CHANGE for the register concept. Re-verified against hubits-api-sales@5274891. The register state model (open = session active / closed = ready), the one-session-per-register constraint, the join rejection on a non-open session [SessionsController.cs:487], and the Counter Open-Sessions/Registers partition are all unchanged. The new PendingClose session status (commit 47912c7 and the 2026-05-08 status-machine work) is a session-level change, not a register-level one; added one clarifying note that a register frees up at close-submit (PendingClose) before approval [SessionsController.cs:1749-1753].
  • 2026-06-14 — TRUTH AUDIT against prod branches (hubits-api-sales@main, foodops.counter.app@main, foodops-admin-app@main, haafai.domain@staging). CONFIRMED in code: one-session-per-register (OpenRegister rejects via GetOpenSessionIdForRegister on IsOpen); register frees at close-submit (CloseRegister sets IsOpen=false, status→PendingClose); Open-Sessions/available-Registers partition (GetMySessionsAndRegisters / GetAllOpenSession); SignalR SessionOpened/SessionClosed org-group broadcasts; soft-delete of registers (DeleteRegister → RecordStatus.Deleted, blocked on open session) and historical-session retention; register-config fields (Name/Description/hours/auto-print) in RegisterFormDialog; template + CashWithdrawalAuthorizationLimit fields on the SalesRegister entity. CORRECTED (were WRONG): Counter landing route is /counter not /orders ("Orders" is only the restaurant sidebar label); register list/detail routes /salesregisters, /salesregisters/details/<id> and the /settings/registers/edit/<id> edit route do NOT exist (registers live under Settings → Restaurant → Outlets, edited in a dialog); Sales Sessions list route is /sales/register-sessions not /salessessions; stale line-number citations refreshed to symbol-based. FLAGGED UNVERIFIED: "Counter role + 33 permission groups" and "lands on Dashboard without Counter role" (no frontend role gate on /counter; role-mismatch shows "Access Denied"; permission-group count not found — belongs to identity service); the distinct "No registers configured / Go to Settings" empty state (CounterPage shows the same "All registers are open." placeholder when the list is empty); template/withdrawal-limit edit UI (fields exist on entity but not surfaced in the located dialog); per-user "cannot hold two concurrent sessions" limit (only per-register enforcement found in code). UI-TODO: re-capture screenshots 121/122 — they show non-existent list/detail pages.
  • 2026-06-29 — LAUNCH RE-VERIFY (new-stack only). Re-pinned to foodops.counter.app@32669a9, hubits-api-sales@19190b9, foodops-admin-app@f6ffd0b, haafai.domain@e138944 (staging). Verdict ACCURATE — spot-re-confirmed all load-bearing facts UNCHANGED at current HEADs: one-session-per-register + "Register already has an open session" (OpenRegister @719, compiled query); CashWithdrawalAuthorizationLimit = 200 + UseReceiptPrinterTemplate on the SalesRegister entity (staging SalesRegister.cs:65,50); admin RegisterFormDialog auto-print labels verbatim ("Print kitchen order tickets automatically" @163 / "Print receipts automatically" @173 / "Print receipt on new order" @185, under "Auto-print" @153); register frees at close-submit (IsOpen=false + status→PendingClose). No haafai.app.foodops citations in this entry (already new-stack). No body change needed.
  • 2026-06-15 — ADVERSARIAL RE-VERIFY of the 2026-06-14 corrections against the same prod branches. UPHELD (re-read in code): one-session-per-register + "Register already has an open session" string (OpenRegister L687-716 / CompiledQueries.GetOpenSessionIdForRegister); CloseRegister sets IsOpen=false + status→PendingClose (L1817/L1821, unconditional); SignalR SessionOpened/SessionClosed to HubGroups.Organization via IHubContext<OrderHub>; /counter route + //counter redirect + restaurant "Orders" relabel (App.tsx L248); ProtectedRoute auth→/login, role-mismatch→"Access Denied" panel; CounterPage "All registers are open." sole empty state + "Open Register"/"Join" button labels; DeleteRegister soft-delete blocked on open session; RegisterFormDialog field set; SalesRegister entity template fields + CashWithdrawalAuthorizationLimit=200; admin routes /settings/restaurant/outlets, /sales/register-sessions; salesRegisterSettingsService BASE. TIGHTENED (correction was directionally right but imprecise): (1) "who sees which registers" — the 2026-06-14 note framed scoping as org-only and cited GetMySessionsAndRegisters (filters by ApplicationOrganisationId); the real filter is org + per-user ApplicationUserOutlets grants in HomeLinkSalesRegisterSession.Initialize (retail-only fallback to all-org-outlets). Re-cited to the actual symbol and restored the per-outlet-access truth. (2) Auto-print toggle labels were paraphrased ("Print KOT"); replaced with the exact UI strings. (3) Noted the settings API does accept template/withdrawal fields (CreateRegister/UpdateRegister) even though the located dialog omits them. CAUGHT (citation lent false code-authority): the "Manager-role user closes it" edge-case workaround carried a JoinSession/CloseRegister citation, but SessionsController is gated only by [ApiKeyOrSessionAuth] with no role attribute on join/close/approve-close — softened to "another authorized user" and flagged the role claim UNVERIFIED. Frontmatter: fixed haafai.domain source path to the nested haafai.domain/Haafai.Models/.... All four commit pins (df086e4 / f79eb7b / 2a42bea / a2bd90e) confirmed to resolve on their cited branches.

Go beyond POS.
Choose the total solution.

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