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.

Where the gap is now

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

  1. Fill in Name — e.g. "Weekend 10% off".
  2. Choose a Type.
  3. Set a Priority (a number; it orders the campaign list).
  4. 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.
  5. Choose Tax interaction: Before tax (reduces taxable base) or After tax (tax stays on full price).
  6. Optionally give a Coupon code (optional) — "Short code cashiers type or scan — e.g. SAVE10, not a UUID."
  7. 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:

TypeWhat it doesWhat 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.

A campaign must carry a value

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:

  1. 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.
  2. 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%.
  3. An exclusive campaign wins outright and stops the rest being considered on that line.
  4. 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.

When it will not fire

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.

  1. Type a code into the new-coupon box (placeholder NEWCODE).
  2. 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."

Fields you cannot set from this page

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.

Redeeming without naming an amount

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.

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 %".

  1. Tap the ticket line to discount.
  2. Press Disc %. The Discount dialog says "Applying to <line>" — "Tap a preset for one-tap apply, or enter a custom percent."
  3. Tap a preset — 5%, 10%, 15%, 20% — or type into Percent.
  4. 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".

A hand-keyed discount always wins

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.

Screenshot to come: the till's Discount dialog with the 5% / 10% / 15% / 20% presets and a selected ticket line.
The discount a cashier applies by hand, on top of anything the campaigns already took off.

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.

What is still missing

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.