← All tests

BOGO — Buy 1 Get 1 Free (prod)

SHIPPED 2026-06-10 · nix-cafe 5473cc3 · migration · Gate 2 6/6 · regression 51/51

The drink-shop promotion from the 06.06 New Features page. A per-shop, cashier-applied discount: for every 2 eligible drinks the cheaper one is free (sort by price, charge the higher). Built in two parts — a config module + the POS cart integration — shipped together. Verified on lumiere-coffee.

The rule

eligible cart UNITS (qty-expanded), sorted by price ↓ for every 2 → pay 1, get 1 free (the FREE ones are the cheapest floor(N/2)) ↓ odd leftover is charged · the higher-priced drinks are always paid Example (spec): A $1.75, B $2.00, C $3.25 → charge C + B, free A → "Buy 1 Get 1 Free −$1.75"

Config — Discount & Loyalty → Discounts

New settings page. Create a Buy 1 Get 1 Free program, scope it to a shop (or all shops) and to eligible categories (or all products), then Activate it. Gate 2 created one and asserted it persisted active in cafe.promotions.

Create promotion modal
Create — name, shop, eligibility
Promotions list
Active promotion in the list

POS cart — Apply the promotion

When an active promo exists for the shop, an Apply «promo» button appears in the cart. Clicking it pairs the eligible units and frees the cheaper of each pair, showing the program name as the discount reason on the freed line + a Remove toggle. Gate 2 added 3× Americano ($3) → applied → 1 freed → net $6 (gross $9), label shown. The reason persists to order_lines.discount_reason and prints on the receipt (default-named promo reads “Buy 1 Get 1 Free”).

Cart before applying
3× Americano = $9 · “Apply” button
Cart after applying BOGO
Applied — 1 freed, net $6, label on the line

Flagged for Narong (defaults, not blockers)

• Eligibility resolves on the cafe-native product model (Starter + cafe-master); Pro odoo-master product ids degrade to ineligible.
• The whole drink (incl. its modifier extra) is freed — spec’s “modifiers not discounted” is a future refinement.
• The pay-dialog order-level discount still stacks on top — no cross-discount guard yet.
• Editing the cart after applying doesn’t auto-recompute — cashier re-taps Apply.