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.
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.
| Case | Before (cheapest) | After (2nd/4th-highest) |
|---|---|---|
| A $1.75 / B $2 / C $3.25 | free A ($1.75) | free B ($2.00) |
| 1. Buy 2 same kind | 1 free | 1 free (same) |
| 2. 1 lower + 1 higher | free lower | free 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 = $5 | free $4+$2 = $6 |
| 5. Buy 1 only | no discount | no discount (same) |
| 6. 2 lower + 2 higher | free 1 low + 1 high | free 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).
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.


| Unit suite — 6 spec scenarios + A/B/C + 07-20 receipt + 07-21 screenshot cart | 9/9 |
| POS: 3 distinct prices → cart total $7 (not $8.50) after Apply | pass |
| POS: FREE badge on the Mocha (2nd-highest) line, NOT the Tea line | pass |
| POS: scenario 6 (2 Tea + 2 Latte) → $7, both freed (regression) | pass |
| POS: Remove promotion clears badges | pass |
| Staging smoke | 5/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.