← All tests

BOGO — same-drink pairing fix STAGING

Reported by Narong 2026-07-20 from a live Get Coffee receipt. Verified on lumiere.staging 6/6 + unit suite 8/8 + staging smoke 5/5.

The bug. The cart was expanded into units, sorted by price, and the cheapest floor(N/2) were freed globally. That's correct for a basket of distinct drinks — but when a customer buys multiples of the same drink, it frees two cheap drinks instead of one of each. On Narong's receipt (4× Get Coffee $2.15, 3× Matcha $2.50, 2× Latte $2.15) it freed 4× Get Coffee and charged $11.80; it should free 2 Get Coffee + 1 Matcha + 1 Latte and charge $11.45.

This was in the spec on day one

The Notion 06.06.2026 spec flagged the exception explicitly — it just wasn't implemented:

"If A x 2 and B x 2, system should discount A & B. By above logic, it would discount 2 B (assuming B is cheaper)"

…and again as Expected Output #6: "2 Lower Price & 2 Higher Price → Free 1 lower price & free 1 higher price."

The fix — two rules, in order

(1) Carve-out: multiples of the same drink pair with each other first — every 2 frees 1 of that drink. (2) Default: the leftovers (≤1 per drink) pair across drinks, freeing the cheapest half. Grouping is by product, so one drink split across lines by modifiers (Iced 50% vs Iced 0%, as on the receipt) still counts as "the same kind". Pairing moved to a pure, unit-tested module lib/promotions/bogo.ts. 2 files, no migration.

Every spec case (unit suite, 8/8)

CaseExpectedBeforeAfter
Spec example A$1.75 / B$2 / C$3.25free A
1. Buy 2 of the same kind1 free
2. 1 lower + 1 higherfree the lower
3. Buy 3 drinksfree lowest, pay 2 highest
4. Buy 4 different drinksfree 2 lowest
5. Buy 1 onlyno discount
6. 2 lower + 2 higherfree 1 lower + 1 higher✗ frees 2 lower
Narong's receiptfree 2 GC + 1 M + 1 L✗ $11.80✓ $11.45

Only case 6 changes — scenarios 1–5 and the worked example are untouched, which is the containment Narong asked for ("it shouldn't override the previous logic"). Freed units are always floor(N/2), asserted in every case.

Live POS proof (lumiere.staging, Starter)

Cart = 2× "BOGO Test Tea" ($2) + 2× "BOGO Test Latte" ($5), gross $14. Old logic → total $10 (frees 2 Teas). Spec logic → total $7 (frees 1 Tea + 1 Latte).

cart before applying
Before Apply — "Promotion available".
both lines freed
After Apply — total $7, and a promo badge on both lines (the old logic could only ever badge the cheap one).

Checks

Unit suite — 6 spec scenarios + worked example + the receipt8/8
POS: cart total $7 (not $10) after Applypass
POS: promo badge on BOTH lines + FREE labelpass
POS: Remove promotion clears badgespass
Staging smoke5/5

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