H3.4's three sub-features verified end-to-end on get-coffee (Pro): Odoo-style per-payment-type layout, Cash In/Out button mounted from inside the close dialog, and the new Daily Sale CSV endpoint actually serving the file (after a parameter-order fix on the first push).
e8a0413 + 14df545.
6 files + 1 new endpoint + 2 new DAOs, no migration.
On prod, clicking "Close shift" now opens the dialog Narong sketched in his screenshot:
header "N orders · $X" on the right, a Cash section with Opening / Payments /
Cash in/out / Counted / Expected sub-rows, one per-method section per non-cash payment
method (Card / KHQR / ABA / Bakong) showing just the total. Title is now "Closing Register"
(was "Close shift"). All five existing testids (close-shift-cash,
close-shift-diff, close-shift-reason, etc.) are preserved so the
regression suite + Slice D's session-close tests still pass.
Clicking the new close-shift-open-cash button opens the shared
CashMovementDialog as a stacked dialog. The close dialog is still in the DOM
underneath (asserted in the test) — closing the cash dialog returns the cashier to the
close screen, ready to enter Cash count and close. Same pattern works on Starter via the
onOpenCashMovements callback the close dialog gets from the parent shell.
New GET /cafe/api/cafe/sessions/[sessionId]/daily-sale returns CSV with the
expected columns (order_number, time, customer,
payment_methods, total_usd, state,
refunded_at); Content-Type text/csv; Content-Disposition
attachment; filename includes session date + short UUID. Click "Daily Sale ⬇"
in the dialog and the browser saves the file natively. Caught + fixed mid-Gate-2:
first push had getNixSessionById(tenant.id, sessionId) — args swapped; the DAO
signature is (id, tenantId), so every fetch 404'd. The dialog still rendered the
correct href; the bug was server-side. One-line fix (14df545), 7/8 → 8/8 on re-run.


CashMovementDialog opens on top, close dialog stays mounted underneathCSV the test downloaded: daily-sale.csv (this run joined a pre-existing session with no orders — the file has just the header row, which is the right behaviour for an empty session).
| test-phase1-prod.mjs | 11/11 |
| test-phase2-sso-outdoor-prod.mjs | 6/6 |
| test-phase2-cafe-multishop-prod.mjs | 6/6 |
| test-m1-prod.mjs | 10/10 |
| test-r7-prod.mjs | 14/14 |
| test-r8-prod.mjs | 4/4 |
| + test-v0-2-slice-e2-prod.mjs | 8/8 |
| Total | 59/59 |
New: app/api/cafe/sessions/[sessionId]/daily-sale/route.ts
Modified: lib/db/orders.ts (+2 DAOs) ·
app/(pos-fullscreen)/pos/register/[configId]/page.tsx ·
app/(pos-fullscreen)/pos/register/[configId]/lockable-shell.tsx (Pro dialog rewrite) ·
app/(authed)/pos/_components/starter-lockable-shell.tsx ·
app/(authed)/pos/_components/starter-close-shift-dialog.tsx (Starter dialog rewrite)