Plan: Sales Order deposit missing from End Shift (v2)

Status: DRAFT. Cause not proven. Not yet re-grilled. Replaces deposit-sales-order-end-shift-plan.html (kept for history; its Steps 1 and 2 are withdrawn). Audit of the old plan: grill-review.md.

The issue in simple words

What a deposit is. A customer orders an item that is not in the shop yet. They pay part of the price now, for example RM 50 of RM 150. The cashier puts the RM 50 in the cash drawer. The customer pays the rest and takes the item later.

What the cashier saw. At the end of the day, the cashier counts the drawer. The count is RM 50 more than the screen says the drawer should hold. The screen looks like it forgot the RM 50 deposit.

What we first thought. The system ignores the deposit because the order is not finished yet.

What we found. That is wrong for a normal day. We checked the live test system. When the customer pays the deposit on the same day, in the same shift as the order, the system counts it. A RM 100 float plus a RM 50 deposit shows RM 150. That is correct.

What probably goes wrong. The system links each deposit to the shift in which the order was made, not to the shift in which the money came in. Look at this case:

DayWhat happensWhat the system does
Monday, shift 1Customer orders. Pays RM 50 deposit.Counts RM 50 in shift 1. Correct.
Tuesday, shift 2Customer comes back. Pays RM 30 more.Counts the RM 30 in shift 1, not shift 2.

The RM 30 is in the drawer of shift 2. Shift 2 does not expect it. The cashier of shift 2 sees RM 30 too much. Shift 1 is already closed, so its numbers are now wrong. Refunds work the same way.

What we do not know yet. We have not seen this happen. The test system has no such case. We must try it by hand first (Phase 0 below). If it does not go wrong, this is not the cause and we look elsewhere.

The fix, if the test shows the problem. Save the shift on each payment, not only on the order. Then every payment counts in the shift where the money came in.

1. What we proved on dev (2026-10-23, read-only)

Database sellable_dev_tenant_db, schema sellable_tenant.

CheckResult
Deployed fn_api_dayshift_get_xreport_shift_summary payment_amounts blockNo doc_type or status filter. Same text as functions_pg.sql:1911-1922.
Open shift 2830: float 100.00 + one DRAFT SO cash deposit 50.00Function returns CLOSE_FLOAT expected 150.00 and XYZ_SALES_DEPOSIT 50.00. The same-shift case works.
Closed shift 2741 (deposits 11.00 + 15.00, float 0)Saved CLOSE_FLOAT expected = 26.00. Saved figures include the deposits.
SO payments taken outside the order's shift window0 rows. Dev has no cross-shift deposit to look at.
DRAFT SOs with deposits15 sampled, all one payment on the order's own day.

So the old plan's claim ("a $50 deposit is dropped because the order is DRAFT") is false for same-shift deposits.

2. Still open: what caused the +$50 on the real till

The code supports one mechanism. Dev data cannot confirm it.

Cross-shift top-up (main suspect). ft_sales_payment has no shift column (tables_pg.sql:3201-3265). Every drawer sum joins on the order header's dayshift_id. A top-up or refund taken in shift 2 on an order made in shift 1 keeps the shift-1 header (list_deposit_dialog_controller.dart:1373-1393). Result: shift 1 (maybe already closed) gets the cash. Shift 2 is short.

Other suspects, each cheap to test:

Phase 0 (do first, no code)

  1. Ask the reporter: was the deposit taken in the same shift as the order? Which tenant? Which app version?
  2. On dev, run this by hand in the app: open shift A, create SO, deposit 50 cash, close shift A. Open shift B, Edit Order, add deposit 30 cash, close shift B.
  3. Run the check below. Expect: shift B CLOSE_FLOAT misses 30; shift A's saved figure is wrong.
-- read-only
SELECT h.doc_ref_no, h.dayshift_id AS header_shift, p.trx_date, p.local_amt, p.tender_category
FROM sellable_tenant.ft_sales_header h
JOIN sellable_tenant.ft_sales_payment p ON p.sales_id = h.sales_id
WHERE h.doc_ref_no = '<SO number>' ORDER BY p.trx_date;
SELECT payment_name, expected_amt FROM sellable_tenant.fn_api_dayshift_get_xreport_shift_summary(<shift B id>)
WHERE payment_name IN ('OPEN_FLOAT','CLOSE_FLOAT','XYZ_SALES_DEPOSIT');

If step 3 does not show the gap, stop. The cause is elsewhere. Do not build Phase 1.

3. Fix (only if Phase 0 confirms): payment-level shift

Decision for the owner: add a shift key to each payment (recommended), or stamp the shift another way. Header-level shift cannot be right for top-ups.

Phase 1 - database (Alembic, prepared only; user runs it)

Phase 2 - API (apps/v2-sellable-api)

Phase 3 - POS client (apps/v2-pos-client/client)

Order of release

Database first (column is nullable, old clients keep working through the COALESCE fallback), then API, then client. Rollback: down-migration restores the old join; the extra column can stay.

4. Tests (must exist before merge)

#CaseExpect
T1Same shift: SO + 50 cash depositExpected = float + 50 (regression guard, passes today)
T2Shift A SO deposit 50; shift B top-up 30A expects 50. B expects 30.
T3Refund of 20 in shift B for a shift-A depositA keeps 50. B shows -20.
T4Void of a deposit in shift BSame as T3
T5Card depositCard total includes it in the payment's shift
T6Foreign-currency depositCounted in its own currency; no 1-cent loss
T7Finalize in shift BB counts only the balance paid. No double count (verified in code: payments are not copied to the invoice).
T8Unsynced SO at End ShiftDecide the rule; test it
T9Close shift: compare_dayshift_summaryX-report and shift summary agree
T10Old row with null dayshift_idFalls back to the header shift

Automated home for DB cases: apps/v2-sellable-bo/backend/tests/unit/repositories/test_business_day_summary_footing_597.py and tests/scenarios/.

5. Not in scope

6. Open questions for the owner

  1. Is the real till problem a cross-shift top-up? (Phase 0 answers it.)
  2. Payment-level shift key (recommended) vs. another way to assign the shift?
  3. Do closed shifts get corrected, or only new ones?