← All tests

BOGO — split-receipt fix STAGING

Reported by Narong 2026-07-21 (get-coffee.staging POS screenshots). Verified on lumiere.staging 6/6 + unit suite 9/9 + staging smoke 5/5.

The complaint. BOGO freed the cheapest half of the eligible drinks. So a customer who split one basket across two receipts could free more than on a single combined receipt ("splitting receipts = save more than 1 full receipt"). On his 4-drink cart (Caffè $3.50, Butter $2.25, Matcha $4, Green $2.50) a single receipt only freed the two cheapest = $4.75 (total $7.50), while pairing the dearest drinks across split receipts frees $5.75 (total $6.50).

The new rule (Narong 2026-07-21)

"Sort items from Highest to Lowest. First Highest Priced Item = Full Price, grants Free Next Highest Priced Item. 3rd Item = Full Price, grants Free for Next Item."

i.e. free every 2nd unit once sorted high→low (the 2nd, 4th, … most expensive). A single receipt now gives the same maximum discount a split would, so there's no reason to split. Money only ever moves further in the customer's favour, so nothing already rung up re-prices upward.

This rule subsumes the 2026-07-20 same-drink carve-out: equal-priced units sort adjacently, so multiples of one drink still pair with each other, and Narong's 07-20 receipt still computes to $11.45. The pairing lives in the pure, unit-tested lib/promotions/bogo.ts. 2 files, no migration.

Unit suite — 9/9

CaseBefore (cheapest)After (2nd/4th-highest)
A $1.75 / B $2 / C $3.25free A ($1.75)free B ($2.00)
1. Buy 2 same kind1 free1 free (same)
2. 1 lower + 1 higherfree lowerfree lower (same)
3. Buy 3 distinct [$5,$4,$3]free $3 (lowest)free $4 (2nd-highest)
4. Buy 4 distinct [$5,$4,$3,$2]free $3+$2 = $5free $4+$2 = $6
5. Buy 1 onlyno discountno discount (same)
6. 2 lower + 2 higherfree 1 low + 1 highfree 1 low + 1 high (same)
Narong 07-20 receipt$11.45$11.45 (same)
Narong 07-21 screenshot cart$7.50 (free $4.75)$6.50 (free $5.75)

Only the distinct-price cases (3, 4, the A/B/C example, and his 4-drink cart) change — from "free the cheapest" to "free the 2nd-highest". Same-price / same-drink cases (1, 2, 5, 6, the 07-20 receipt) are byte-identical. Freed units are always floor(N/2).

Live POS proof (lumiere.staging, Starter)

The decisive cart uses 3 distinct prices: Tea $2 + Mocha $3.50 + Latte $5 (gross $10.50). Old logic frees the lowest (Tea) → total $8.50; the new rule frees the 2nd-highest (Mocha) → total $7.00.

Mocha freed, total $7
After Apply — total $7, and the FREE badge is on the Mocha ($3.50) line. The Tea ($2) is charged.
scenario 6 unchanged
Regression guard — 2 Tea + 2 Latte still totals $7 with a badge on both lines (same-price case unchanged).

Checks

Unit suite — 6 spec scenarios + A/B/C + 07-20 receipt + 07-21 screenshot cart9/9
POS: 3 distinct prices → cart total $7 (not $8.50) after Applypass
POS: FREE badge on the Mocha (2nd-highest) line, NOT the Tea linepass
POS: scenario 6 (2 Tea + 2 Latte) → $7, both freed (regression)pass
POS: Remove promotion clears badgespass
Staging smoke5/5

nix-cafe staging 38383fd (Worker nix-cafe-staging, version 672e4a3d). Starter tenant required — BOGO eligibility resolves cafe-native product UUIDs, so Pro/Odoo ids degrade to ineligible. Parked for the scheduled prod promote.