---
name: lis-cashier-shift
description: How the LIS cashier SHIFT (cash-drawer session) works — open/close mechanics, the AUTOMATIC payment↔session link, and the key fact that a shift is OPTIONAL (collections without an open shift have cashier_session_id=NULL and still record). Basis for the cashier shift report.
updated: 2026-06-29
---

# LIS Cashier Shift (Cash-Drawer Session)

## TL;DR — the shift is OPTIONAL
A cashier **can collect money without ever opening a shift.** When a payment is created the model auto-stamps it with the cashier's currently-open session; **if none is open, `lab_payments.cashier_session_id` stays NULL** and the payment records normally — it just isn't tied to any shift. So any "cashier report" MUST handle both: payments inside a shift (tagged) AND un-shifted collections (NULL).

## The pieces
- **Header widget** `features/lis/cashier-shift-widget` — one-click Open/Close on every LIS screen.
- **Page** `/lab/cashier-shift` (`features/lis/cashier-shift/lis-cashier-shift.component.ts`) — open a shift, see the current shift's by-method summary, close & reconcile, list past shifts.
- **Reconciliation page** `/lab/cashier-reconciliation` — per-cashier × method + expected cash + drill-down receipts (a DAY view, not shift-scoped).
- **Cash-flow page** `/lab/cash-flow` — opening float + collections + petty-cash expenses.
- **BE** `LisCashierSessionController` + model `LisCashierSession` (table `lis_cashier_sessions`).

## Data model
- **`lis_cashier_sessions`**: `id, company_id, branch_id, user_id, treasury_id, opening_float, expected_cash, counted_cash, over_short, status('open'|'closed'), opened_at, closed_at, opened_by, closed_by, notes`.
- **`lab_payments.cashier_session_id`** (nullable, added by migration `2026_05_31_120000`) — the link. **A session's payments = `WHERE cashier_session_id = s.id AND status=completed`.**

## Lifecycle
1. **Open** (`POST cashier-sessions/open`): cashier enters the **opening float** (cash already in the drawer). The shift ties to the cashier's **branch** + that branch's **PettyCash cash box** (`treasury_id`, resolved from `accounting.PettyCash where branch_id`). Only **one open session per user** (else 422).
2. **Collect**: every `LabPayment::creating` runs a hook (`Models/LabPayment.php:~32`) → `cashier_session_id = LisCashierSession where(user_id=auth, status=open, latest).id`. **No open session → NULL** (un-shifted, still valid).
3. **Close** (`POST cashier-sessions/{id}/close`): cashier counts the drawer → `counted_cash`. System computes **`expected_cash = opening_float + cash_collected_this_session`** and **`over_short = counted − expected`** (the Z-report). **Only CASH counts toward the drawer** — visa/tamara/tabby/bank_transfer go to the bank, never the drawer (`cashTotal()` filters `method==='cash'`).

## Endpoints (`Modules/LIS/routes/api.php`)
`GET cashier-sessions/current` · `POST cashier-sessions/open` · `POST cashier-sessions/{id}/close` · `GET cashier-sessions` (index) · `GET cashier-sessions/{id}` (show) · `GET cashier/reconciliation?date=&cashier_id=&branch_id=`. Permission gate `lis.payments` / `lis.payments.create`.

## Session shape (what the FE gets)
`shape(session)` → `by_method[{method,count,total}]`, `collected_total`, `cash_collected`, `expected_cash` (= opening_float + cash while open; the stored value once closed), `over_short`, plus opening_float/opened_at/closed_at/status.

## 🔑 Gotchas / design notes for the report
- **Optional shift** → a day's collections = a MIX of shifted (a session) + un-shifted (NULL). A complete report needs a **day view that includes BOTH** (group by session, plus a "no shift" bucket), not only per-session. **📊 Measured 2026-06-29: ALL 62 completed payments on `moonui_dev_be` have `cashier_session_id=NULL`** — i.e. nobody actually opens shifts; the "no-shift" bucket is the PRIMARY case, not an edge case. A shift-only report would show nothing.
- **Verified data shapes (2026-06-29):** every completed payment has a `receiving_account_id` → treasury name (0 missing: cash→"jedda. cash", visa→"Visa / Network", tamara, tabby). Receipt-detail fields all present: `payment_number`, `date` + `created_at` (time), `lab_invoice_id`→`invoice_number`, `patient_id`→name, `payment_method`, `amount`, `status`. No Z-report/print exists yet.
- **Drawer = cash only.** Card/wallet/transfer collections belong to the shift's totals but NOT the cash reconciliation (`expected_cash`). The report must keep "cash in drawer" separate from "total collected".
- **One treasury per shift** (the branch cash box). Per-method treasuries (visa→bank acct, etc.) come from the payment's `receiving_account_id`, not the session.
- **Cashier = `lab_payments.created_by`** (the user who recorded the receipt). Self-vs-all scoping uses `LisDataScope` (own → created_by=self).
- The reconciliation endpoint is already per-cashier×method; the **shift report adds**: receipt-level detail grouped by payment type, day selection, print, and self/all permission. Plan: `moonui.elbaset.com/lis-cashier-report-plan.html`. See [[staging-mirror]] for testing.

## ✅ BUILT (2026-06-29) — cashier day-collection report
**On `hazemdev`+`main`, deployed to moonui `/app` (⏳ NOT on staging/clients — owner ships via MoonStack update).** Endpoint **`GET /api/lis/cashier/day-report?date=&cashier_id=`** (`LisCashierSessionController::dayReport` + `reportGroup`, BE `20b412ff9`) → the day's completed payments grouped by **cashier → shift (+ a `no_shift` bucket) → payment type**, each type carrying its **treasury** (`receivingAccount.name`) + **receipts** (payment_number/time/invoice_number/patient/amount); each shift also returns its **Z-report** (opening_float/expected_cash/counted_cash/over_short). Permission-gated `lis.payments.view`; **self/all via `LisDataScope`** (own → `created_by=self`, `can_view_all=false`, cashier filter hidden; broader → optional `cashier_id`). **FE** (`cashier-shift` page, FE `fcf455e`): day picker + manager cashier dropdown + by-type display + **2 print views** (summary/details, A4 string-built HTML with a hand-over/received signature). Native `code-reviewer` APPROVE after fixing 1 HIGH (the endpoint was missing the `lis.payments.view` guard) + a load-error toast. No migration (reads existing tables).
