Promotions
A campaign you set up in the back office now comes off at the till on its own — no coupon typed, nobody remembering. This page is about how a campaign is judged, which ones still wait to be typed in, and the one piece that is not on the screen yet.
Campaigns fire automatically, but the New campaign form on this page has no field for how much — no percentage, no amount, no buy-and-get boxes. The API refuses a campaign that cannot pay out (A percent promotion needs a percentage), so a campaign created from this form is rejected until its value is set another way.
There is also still no coupon box at the till. A cashier cannot type SAVE10 anywhere in the POS; coupon redemption happens over the API. What a cashier can do by hand is a percentage off one line (Disc %).
The Promotions page
Promotions — "Create, edit, and delete campaigns and coupon codes for this store." It is an add-on; if it is missing, turn Promotions on in Settings → Add-ons.
The page explains itself: "Create store discounts and coupon codes cashiers can apply at the register. Example: a weekend 10% off campaign with coupon code SAVE10, then pause or delete it when the promo ends. Scope by product or category names — never paste raw IDs."
Four sections: New campaign, Campaigns, Edit campaign and Coupons.
Create a campaign
- Fill in Name — e.g. "Weekend 10% off".
- Choose a Type.
- Set a Priority (a number; it orders the campaign list).
- Choose Applies to: Entire basket, Specific products or Specific categories. The picker searches by name — "Milk, bread…", "Dairy, beverages…" — so you never paste an ID.
- Choose Tax interaction: Before tax (reduces taxable base) or After tax (tax stays on full price).
- Optionally give a Coupon code (optional) — "Short code cashiers type or scan — e.g. SAVE10, not a UUID."
- Press Create campaign.
The step that is missing from that list is how much. Until the form grows those boxes, pressing Create campaign comes back with the refusal for whichever value the type needs — see the next section.
The types on offer
The Type dropdown lists five values, shown as their raw internal names. All five now have an engine behind them, and each needs its own figures before it will be accepted:
| Type | What it does | What it must carry |
|---|---|---|
percent |
A percentage off each line it covers. | A percentage. |
fixed |
An amount off the line, not off each unit. "£1 off" on a line of three is £1, which is what the shelf edge means. | An amount. |
bogo |
Buy N, get M at a discount. It counts complete groups: on buy-one-get-one, one item earns nothing, two earn the free one, and three still earn one — not one and a half. | Buy how many, get how many, and how much off the free ones (100 for buy-one-get-one-free). |
basket_threshold |
Spend this much, get that off the whole ticket. Judged after the line-level discounts, so the order items were scanned in cannot change the answer. | A spend to reach, plus a percentage or an amount to give back. |
coupon |
Waits to be typed in. The till never volunteers it. | Its value, and a code. |
A percentage campaign can also carry a ceiling, so 20% off does not become a giveaway on the most expensive thing in the shop.
Saving a campaign with no figures is refused, in the words of whichever box is empty: A percent promotion needs a percentage, Buy how many?, Spend how much?. A campaign that cannot pay out is worse than no campaign — it advertises a saving on the shelf edge and delivers nothing at the till.
The one place this still bites: the New campaign form on this page does not have those boxes yet, so it cannot create a campaign that passes.
A campaign with a coupon code stays manual
Attach a coupon code to a campaign and it stops firing on its own —
whatever its type. SAVE10 is a code a customer produces, and a till that handed
it to everybody who walked in would be giving the shop's money away. That is
true of the coupon type and equally of a percent
campaign somebody attached a code to.
How a campaign comes off at the till
Every time an item is added to a ticket — and when a customer is attached or removed — the till works out the whole ticket's discounts again from scratch. Rebuilt rather than added to, because a line taken out of the basket can take a threshold promotion with it, and a discount that outlives the thing that earned it is how a till starts giving money away.
Four rules decide it, and they are deliberately few, because pricing that cannot be explained at the counter is pricing nobody trusts:
- A discount somebody keyed in by hand is final. The person who typed 50% was standing in front of the customer and knew something the rules do not. Nothing recalculates it away.
- Otherwise the customer gets whichever is better for them — the campaign, or their customer group's standing discount — never both on one line. Two rules that each take 20% off do not take 40%.
- An exclusive campaign wins outright and stops the rest being considered on that line.
- Basket thresholds are judged last, on what the ticket costs after the line-level work.
Whatever fires is recorded by name, so the receipt can say why the money came off and the month-end report can tell a campaign from a favour. The customer's copy also gets a plain You saved $2.50 today under the total — see Receipts.
A campaign is only live inside its dates and while Active is ticked: one that ended yesterday stops the same night, one starting next week does nothing today. A campaign scoped to specific products or categories with nothing actually selected matches nothing — the opposite reading would turn a half-finished campaign into a store-wide sale.
What the scope and priority do
Applies to is honoured: a campaign scoped to specific products only touches lines holding those products, and a category-scoped one only touches lines in those categories. Priority orders the list of campaigns considered; where two could both fire on a line and neither is exclusive, the one worth more to the customer is the one that lands.
Tax interaction is recorded but does not change the draft arithmetic; the discount comes off the subtotal and the tax figure is left alone until the sale is finalized.
Coupons
Select a campaign, then use the Coupons section.
- Type a code into the new-coupon box (placeholder NEWCODE).
- Press Add coupon. You get "Coupon SAVE10 added".
Codes are 2–32 characters of letters, numbers, hyphen or underscore, and are stored in capitals. Typing something that looks like an ID is rejected — "Use a short code like SAVE10 — not a UUID". Codes are unique across the whole company: a duplicate gives "Coupon code already exists for this org".
Each coupon row shows its redemption count as "3 / 100 used", and a redeemed coupon cannot be truly deleted — deleting one that has been used deactivates it instead ("Coupon SAVE10 deactivated (already redeemed)"). The same is true of campaigns: "Unused campaigns are removed; used ones are deactivated."
Every coupon is created as multi-use with no maximum redemptions, and there are no start or end date fields anywhere on the page. Single-use and per-customer coupons, redemption caps and validity windows all exist in the data model but have no controls. A campaign runs until you untick Active and press Save changes, so plan to switch a promotion off by hand on the day it ends.
How a redeemed coupon lands on a ticket
A coupon is redeemed against a whole order rather than a line, and it is a separate path from the automatic campaigns above. There is no box for it in the POS today, so redemption happens over the API.
If a redemption does not say how much to take off, the coupon path still
falls back to a flat 10% on a percent campaign
and a flat 5.00 on anything else — it does not yet read the
campaign's own value the way the automatic evaluator does. Name the amount on
the redemption, or expect those two figures.
- The discount is recorded against the whole order, never against a particular line.
- It is added to the order's discount total; the new total is subtotal − discounts + tax, and never goes below zero.
- A discount is capped at the subtotal — you cannot discount an order into a negative.
- The same coupon cannot be redeemed twice on one order ("Coupon already applied to this order"). Different coupons stack, additively.
- The order must be open or held. On anything else you get "Cannot apply coupon to status=completed".
- An exhausted, inactive or out-of-window coupon is refused with "Coupon is exhausted, inactive, or outside validity window".
What the cashier sees
A campaign that fires needs nothing from the cashier at all: the line simply shows what came off, and the pay summary counts it in Discount. There is nothing to press and nothing to remember.
The one discount a cashier applies themselves is Disc % on the ticket actions — "Discount selected line". With nothing selected the tooltip nudges: "Tap a ticket line, then Disc %".
- Tap the ticket line to discount.
- Press Disc %. The Discount dialog says "Applying to <line>" — "Tap a preset for one-tap apply, or enter a custom percent."
- Tap a preset — 5%, 10%, 15%, 20% — or type into Percent.
- Press Apply. The line shows "−2.50 off" and a toast confirms "10% off · Flat White".
Anything outside 0–100 is refused: "Discount must be between 0 and 100%". The discount is recorded against that line with the reason "cashier discount".
Once a cashier has typed a discount onto a line, no campaign, standing discount or later recalculation touches it. Scan the next item and it is still exactly what they typed. Software that silently reverses a decision somebody made in front of a customer is software the shop floor stops trusting.
In the pay summary the cashier sees Subtotal, then Discount shown as a negative figure, then tax — each rate by name when there is more than one, otherwise a single Tax line.
Watching what discounting costs you
Every discounted ticket lands in Reports → Voids & discounts, which lists completed sales whose discount total is greater than zero (and separately, voided orders). That is the honest way to see how much is coming off tickets, whoever authorised it. See Reports.
Every discount is filed with the name of whatever caused it — the campaign's own name, the customer group's, or "cashier discount" — so that report can tell a planned promotion from a favour.
Two things, both on the screen rather than in the engine: the New campaign form has no boxes for the campaign's value, and there is no place at the till to type a coupon code. Everything else on this page happens on its own.