Skip to content

Offers & Promotions

Overview

Offers and Promotions allow merchants to create discounts and deals. An offer applies either to specific items/groups, to the whole order, or as a Buy-X-Get-Y (BOGO) deal. Discounts can be a percentage, a fixed amount, or "free" (BOGO only), and are applied automatically when conditions are met or unlocked with a discount code.

Key Purpose: Create promotional offers to attract and retain customers.

Purpose

This page lets you create, schedule, and manage promotional offers that apply discounts to customer orders based on configurable conditions such as item quantity, order amount, or buy-X-get-Y rules.

Key Concepts

  • Offer Types (type, enum OfferTypes): Three types exist -- ITEMS (discount on specific items or display groups), ORDER (discount on the whole cart), and BOGO (buy X items, get Y items discounted or free).
  • Offer Methods (method, enum OfferMethods): An offer is either Automatic (auto-applied when conditions are met) or Code (requires the customer to enter a unique discount code at checkout). The discount_code field is only collected for the Code method.
  • Offer Statuses (status, enum OfferStatuses): Offers follow a lifecycle of Draft, Scheduled (enum case UPCOMING), Active, Expired, and Archived. Apart from manually-set Draft and Archived, the status is computed automatically from the configured date range and the total usage limit.
  • Minimum Purchase Validation (minimum_purchase_type, enum OfferMinimumPurchaseTypes): Offers can require no-minimum-requirements, a minimum-quantity-items (with minimum_quantity_items), or a minimum-purchase-amount (with minimum_purchase_amount).
  • Offer Combination (can_be_combined, bool): If false, the offer cannot be applied alongside other offers in the same order.

Actions

Create an Offer

First choose a type (Amount of items / Amount of order / Buy X get Y) in the "Select offer type" modal -- the type cannot be changed afterwards. Then define the offer name, discount method (Automatic or Code), discount unit and value, what it applies to (specific groups or specific items), combination rule, minimum purchase requirements, usage limits, availability channels, active days, and the active date range.

Activate an Offer

A Scheduled offer has an Activate action that makes it live immediately (POST .../activate/{id}).

Archive and Unarchive Offers

To take a live offer down there is no "Deactivate" button in the list view -- use Archive (POST .../archive/{id}), available on Active, Scheduled, and Archived rows. Archiving hides the offer from active listings without deleting it. Unarchive (POST .../unarchive/{id}) is available on Archived rows and restores the offer to its computed current status based on its date range.

Delete an Offer

Permanently remove an offer and its associated item/group relations from the system.

Location

  • Backoffice Route: /marketing/offers
  • List View: src/views/marketing/offers/Offers.vue (mounted via src/pages/marketing/offers/index.vue)
  • Create/Edit Form: src/views/marketing/offers/CreateOfferForm.vue (with step components under components/create-offers/)
  • Store Module: src/store/modules/offers.ts
  • Backend Controller: app/Http/Controllers/Api/OfferController.php
  • Backend Request: app/Http/Requests/BackOffice/Offer/StoreOfferRequest.php
  • Backend Model: app/RawModels/Offer.php

Offer Types

The type field (OfferTypes enum) has exactly three values:

Amount of items (ITEMS)

Discount on specific items or display groups. Uses a discount unit of percentage or fixed amount.

Amount of order (ORDER)

Discount applied to the whole cart total. Uses a discount unit of percentage or fixed amount.

Buy X get Y / BOGO (BOGO)

Customer buys a required quantity of qualifying items/groups and gets a benefit quantity of items/groups at a discount. The discount unit here is percentage, fixed amount, or free (DiscountUnitsExpanded).


Fields

All fields below come from StoreOfferRequest::rules() and the Offer model constructor. Many fields are conditional on type, method, offer_applies_to, minimum_purchase_type, and discount_unit. When is_draft is true every field becomes nullable; otherwise the listed fields are required.

Offer Name

PropertyValue
Field IDname
TypeText (string)
RequiredYes
ValidationRequired, must be unique within the location (UniqueInConnectionWithModel). An offer keeps its own name when editing.

Description: Name of the offer. There is no character-limit rule in StoreOfferRequest.


Offer Type

PropertyValue
Field IDtype
TypeSelect (enum OfferTypes)
RequiredYes
OptionsITEMS (Amount of items), ORDER (Amount of order), BOGO (Buy X get Y)

Description: Chosen in the "Select offer type" modal and cannot be changed after creation.


Method

PropertyValue
Field IDmethod
TypeSelect (enum OfferMethods)
RequiredYes
OptionsAutomatic, Code

Description: Automatic applies when conditions are met; Code requires the customer to enter a discount code at checkout.


Discount Code

PropertyValue
Field IDdiscount_code
TypeText (string)
RequiredRequired only when method = Code
ValidationMax 10 characters (front-end rule), must be unique within the location (UniqueInConnectionWithModel).

Description: Code the customer enters to unlock the offer. The "Generate random code" button fills an 8-character A–Z/0–9 code. Only collected for the Code method.


Discount Unit

PropertyValue
Field IDdiscount_unit
TypeSelect
RequiredYes (for ITEMS, ORDER, and BOGO)
OptionsITEMS / ORDER: percent, amount (enum DiscountUnits). BOGO: percent, amount, free (enum DiscountUnitsExpanded).

Description: Whether the discount is a percentage, a fixed amount, or (BOGO only) free.


Discount Value

PropertyValue
Field IDdiscount_value
TypeNumber
RequiredRequired when discount_unit is percent or amount (not used for free)
Validationpercent: numeric, > 0, <= 100. amount: numeric, > 0.

Description: The discount magnitude. Stored as a Money value for amount discounts, a float for percentages.


Offer Applies To (ITEMS only)

PropertyValue
Field IDoffer_applies_to
TypeSelect (enum OfferAppliesTo)
RequiredRequired for ITEMS offers
Optionsspecific-groups, specific-items

Description: For ITEMS offers, whether the discount targets display groups or individual items.

  • If specific-groups: display_groups (array of { id }, validated against DisplayGroup) is required.
  • If specific-items: items (array of { id }, validated against Item) is required.

Minimum Purchase Type

PropertyValue
Field IDminimum_purchase_type
TypeSelect (enum OfferMinimumPurchaseTypes)
RequiredYes (for ITEMS and ORDER any of three; BOGO restricted to the two non-empty options)
Optionsno-minimum-requirements, minimum-quantity-items, minimum-purchase-amount

Description: The minimum-spend requirement. For BOGO only minimum-quantity-items and minimum-purchase-amount are allowed.

  • If minimum-purchase-amount: minimum_purchase_amount (numeric, > 0) is required.
  • If minimum-quantity-items: minimum_quantity_items (numeric, > 0) is required.

Can Be Combined

PropertyValue
Field IDcan_be_combined
TypeBoolean
RequiredYes

Description: If false, the offer cannot be applied alongside other offers in the same order.


Usage Limits

Field IDTypeRequiredDescription
limit_discount_usageBooleanYesEnable a total redemption cap across all customers.
limit_discount_usage_amountNumber (> 0)Required when limit_discount_usage is trueThe total redemption cap. When reached, the offer is computed as Expired.
limit_one_usage_per_customerBooleanYesIf true, a customer who already redeemed the offer is blocked from redeeming it again.

Availability & Schedule

Field IDTypeRequiredDescription
availabilityArray of enum ChannelOptionsYesChannels the offer is available on: Kiosk, POS, Online Ordering, Table Qr Ordering, Uber Eats, Takeaway, Shopify.
active_daysArray of weekday namesYesDays of week the offer is active (validated against Carbon::getDays()).
active_dates.start_dateDate Y-m-dYesStart date.
active_dates.start_timeTime H:iYesStart time.
active_dates.set_end_dateBooleanYesWhether the offer has an end date.
active_dates.end_dateDate Y-m-dRequired when set_end_date is trueEnd date.
active_dates.end_timeTime H:iRequired when set_end_date is trueEnd time.

Status & Draft

Field IDTypeRequiredDescription
is_draftBooleanNoWhen true, the offer is saved as a Draft and all other fields become nullable.
statusString (enum OfferStatuses)Set to Draft automatically when is_draft is trueOtherwise the effective status is computed by getCurrentStatus().
location_idStringYesThe location the offer belongs to.

BOGO (Buy X Get Y) Fields

These fields apply only when type = BOGO.

Customer Buys (requirement)

Field IDTypeRequiredDescription
requirement_applies_toEnum OfferAppliesTo (specific-groups / specific-items)YesWhat the customer must buy to qualify.
requirement_display_groupsArray of { id }Required when requirement_applies_to = specific-groupsQualifying display groups.
requirement_itemsArray of { id }Required when requirement_applies_to = specific-itemsQualifying items.

The minimum (quantity of items or purchase amount) is set via minimum_purchase_type as described above.

Customer Gets (benefit)

Field IDTypeRequiredDescription
benefit_quantityNumber (> 0)YesHow many benefit items the customer gets.
benefit_applies_toEnum OfferAppliesTo (specific-groups / specific-items)YesWhat the benefit applies to.
benefit_display_groupsArray of { id }Required when benefit_applies_to = specific-groupsBenefit display groups (stored as benefit_display_group_ids).
benefit_itemsArray of { id }Required when benefit_applies_to = specific-itemsBenefit items (stored as benefit_item_ids).
discount_unitEnum DiscountUnitsExpanded (percent / amount / free)YesDiscount applied to the benefit items.
enable_max_usage_per_orderBooleanYesWhether to cap how many times the deal applies per order.
max_usage_per_orderNumber (> 0)Required when enable_max_usage_per_order is trueThe per-order cap.

Business Logic

Offer Validation

Customer applies offer


Check offer status (getCurrentStatus):
├── Draft or Archived? → not redeemable
├── Before start date/time? → Scheduled (not yet valid)
├── Total usage limit reached? (limit_discount_usage_amount) → Expired
├── Past end date/time (if set_end_date)? → Expired

└── Computes to Active → Continue


Check redemption + order conditions:
├── limit_one_usage_per_customer and already redeemed? → blocked
├── minimum_purchase_type met? (quantity of items or purchase amount) → else not applied
├── Qualifying items/groups in cart? → else not applied
├── Channel in `availability`? → else not available on this channel
├── Active on today's weekday (`active_days`)? → else not active today

└── All conditions met → Apply discount

Discount Calculation

Percentage (discount_unit = percent):
- Discount = Eligible Amount × (discount_value / 100)

Fixed Amount (discount_unit = amount):
- Discount = discount_value
- Cannot exceed the eligible amount

Free (BOGO only, discount_unit = free):
- Benefit items become free

ORDER type:
- Whole-cart discount distributed proportionally across all cart items
  based on each item's share of the cart total

BOGO type:
- Customer buys the required quantity of qualifying items/groups
- benefit_quantity benefit items receive the discount
- enable_max_usage_per_order can cap how many times the deal applies per order

Combination Rules

Can multiple offers be combined on one order?
├── Each offer carries a can_be_combined flag
├── If every applied offer has can_be_combined = true → allowed
└── If any applied offer has can_be_combined = false and more than one
    offer is applied → the order is rejected

Customer Impact

Online Ordering

  1. Discount Code Entry: Field at checkout (for Code offers)
  2. Automatic: Discount shown automatically (for Automatic offers)
  3. Available Offers: List of applicable offers
  4. Validation: Error messages for invalid codes
  5. Discount Display: Discount line in order summary

Kiosk

  • Discount code entry (for Code offers)
  • Automatic offers applied when conditions are met
  • Discount shown on screen

Receipt

Subtotal:           €25.00
Discount (SUMMER20): -€5.00
Total:              €20.00

Relations

Depends On

  • Items: For item-specific offers (items / requirement_items / benefit_items)
  • Display Groups: For group-specific offers (display_groups / requirement_display_groups / benefit_display_groups)
  • Customers: For per-customer usage tracking (OfferRedemption)

Affects

  • Online Ordering: Discount at checkout
  • Kiosk: Discount applied
  • Transactions: Discount recorded
  • Reports: Offer usage analytics

Business Rules

  • Discount codes must be unique across all offers; the system rejects a new or updated offer if its discount code is already used by another offer.
  • When an offer has "limit discount usage" enabled and the total redemption count reaches the configured limit, the offer is no longer redeemable by any customer.
  • If "limit one usage per customer" is enabled, the system checks the offer redemption repository for an existing record matching the customer and offer; a second redemption is blocked.
  • For whole-cart (ORDER type) discounts, the discount is distributed proportionally across all items in the cart based on each item's share of the total cart price.
  • Status is computed by getCurrentStatus(): Draft and Archived are sticky (never recomputed); otherwise an offer is Scheduled before its start, Expired once its total usage limit is hit or its end date passes, and Active in between. A Scheduled offer can be activated early via the Activate action; a live offer is taken down via Archive (there is no "deactivate" action in the list view).

FAQs

  • "Can a customer combine multiple offers on one order?" Only if all selected offers have the "can be combined" flag enabled; if any offer disallows combination and more than one offer is applied, the system rejects the request.

  • "What is the difference between an Offer and a Coupon in this system?" In the backend, coupons are implemented as offers with the method set to "Code" and a unique discount code; there is no separate coupon service.

  • "How does Buy X Get Y work with display groups?" The system checks that the requirement display group contains enough qualifying items, then applies the discount to benefit items in the specified benefit display group or specific benefit items.

  • "What happens when an offer is archived?" The offer status is set to "Archived" and it is excluded from active offer queries; unarchiving restores it to its computed current status based on its date range.

  • "Are discounts applied before or after tax?" Currently, discounts are applied before tax calculation, though the codebase contains TODO annotations for a future option to apply discounts after tax.

  • "What are the offer types I can create, and how do I pick one?" Click "Create offer" and choose a type in the "Select offer type" modal: Amount of items (discount on specific items/groups), Amount of order (a full-order discount), or Buy x, get Y (a BOGO-style deal). The type can't be changed after this step, so pick carefully.

  • "What's the difference between an Automatic discount and a Discount Code offer?" On the offer information step you pick a method: Automatic Discount applies when conditions are met, while Discount Code requires the customer to enter a code at checkout. The code field (and "Generate random code") only appears for the Code method.

  • "How long can my discount code be, and can I auto-generate one?" A discount code is max 10 characters. You can type your own or use "Generate random code", which fills in a random 8-character alphanumeric code.

  • "Can two offers use the same discount code or name?" No. Discount codes and offer names must be unique within your location; a new or edited offer with a code or name already used elsewhere is rejected (an offer keeps its own code/name when editing).

  • "How do I let (or stop) customers from stacking this offer with others?" On the Combinations card pick "Can be combined with other offers" or "Cannot be combined with other offers." If set to cannot-combine and a customer applies more than one offer, the order is rejected.

  • "How do I require a minimum spend or quantity before an offer applies?" On the Minimum purchase card choose No minimum, Minimum quantity of items (enter a quantity), or Minimum purchase amount (enter an amount). For BOGO the same minimum is set under "Customer buys."

  • "What do the offer statuses (Draft, Scheduled, Active, Expired, Archived) mean?" Status is computed automatically: a saved draft stays Draft; before its start date it's Scheduled; while live it's Active; after its end date or once a total usage limit is hit it's Expired; Archived is set manually. The list has tabs for All, Active, Scheduled, and Expired.

  • "Can I make a Scheduled offer go live early, and how do I pause a live one?" Yes. A Scheduled offer has an Activate action that makes it Active immediately. To take a live offer down, use Archive (there's no "deactivate" button). Archived offers can be restored with Unarchive, which recomputes the status from the date range.

  • "For a 'Buy X Get Y' offer, how do I set what the customer must buy versus get?" Under Customer buys set the required quantity/amount and whether it applies to specific groups or items. Under Customer gets set the get quantity, the benefit groups/items, and the discount on them (Percentage, Fixed amount, or Free). You can cap how many times it applies per order.

  • "Can I limit how many times an offer is used overall or per customer?" Yes. Enable a total usage limit (once total redemptions reach it, the offer auto-expires for everyone) and/or a one-use-per-customer limit (a customer who already redeemed it is blocked).

  • "For a whole-order discount, how is the discount split across items?" An Order-type discount is distributed proportionally across all items in the cart, with each item taking a share based on its portion of the cart total.

  • "Are discounts taken off before or after tax?" Discounts are currently applied before tax. There's no setting to apply them after tax yet.

Troubleshooting

Problem: Discount code not working

Causes:

  1. Code entered incorrectly
  2. Offer expired (past end date or total usage limit reached)
  3. Offer not yet started (Scheduled)
  4. Minimum purchase requirement not met
  5. Qualifying items/groups not in cart
  6. Channel not in the offer's availability, or not active on today's weekday

Solutions:

  1. Check exact code spelling
  2. Verify the active date range and total usage limit (limit_discount_usage_amount)
  3. Verify the start date/time
  4. Meet the minimum_purchase_type requirement
  5. Add qualifying items/groups to the cart
  6. Verify availability and active_days

Problem: Automatic offer not applying

Causes:

  1. Method is Code, not Automatic
  2. Conditions not met (minimum purchase, qualifying items/groups)
  3. Offer status is not Active (Draft, Scheduled, Expired, or Archived)
  4. Channel not in availability, or wrong weekday in active_days

Solutions:

  1. Confirm the method is Automatic
  2. Check all conditions
  3. Check the computed status / activate or unarchive as needed
  4. Verify availability and active_days

Problem: Discount amount wrong

Causes:

  1. Only some items/groups are eligible
  2. ORDER discount spread proportionally across cart items
  3. Percentage vs fixed amount confusion

Solutions:

  1. Verify offer_applies_to and the selected items/groups
  2. For ORDER offers, the discount is split across all items by share of total
  3. Confirm discount_unit and discount_value

Problem: Customer used offer multiple times

Causes:

  1. limit_one_usage_per_customer not enabled
  2. Different accounts used
  3. Guest checkout (no tracking)

Solutions:

  1. Enable limit_one_usage_per_customer
  2. Require account for offers
  3. Track by email/phone

Examples

These payloads mirror the StoreOfferRequest schema. Items and display groups are passed as arrays of { "id": "..." }.

Order discount, automatic, percentage

json
{
  "location_id": "664a1f...",
  "type": "ORDER",
  "method": "Automatic",
  "name": "Summer Sale 20% Off",
  "discount_unit": "percent",
  "discount_value": 20,
  "minimum_purchase_type": "minimum-purchase-amount",
  "minimum_purchase_amount": 15,
  "can_be_combined": false,
  "limit_discount_usage": false,
  "limit_one_usage_per_customer": false,
  "availability": ["Online Ordering", "Kiosk"],
  "active_days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
  "active_dates": {
    "start_date": "2026-06-01",
    "start_time": "00:00",
    "set_end_date": true,
    "end_date": "2026-08-31",
    "end_time": "23:59"
  }
}

Result: 20% off the whole order, auto-applied, when the cart is at least €15.


Order discount, discount code, fixed amount

json
{
  "location_id": "664a1f...",
  "type": "ORDER",
  "method": "Code",
  "discount_code": "WELCOME5",
  "name": "€5 Off First Order",
  "discount_unit": "amount",
  "discount_value": 5,
  "minimum_purchase_type": "minimum-purchase-amount",
  "minimum_purchase_amount": 20,
  "can_be_combined": false,
  "limit_discount_usage": false,
  "limit_one_usage_per_customer": true,
  "availability": ["Online Ordering"],
  "active_days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
  "active_dates": {
    "start_date": "2026-06-01",
    "start_time": "00:00",
    "set_end_date": false
  }
}

Result: €5 off when the code WELCOME5 is entered on an order of at least €20, once per customer.


Items discount, automatic, specific groups

json
{
  "location_id": "664a1f...",
  "type": "ITEMS",
  "method": "Automatic",
  "name": "20% Off Burgers",
  "discount_unit": "percent",
  "discount_value": 20,
  "offer_applies_to": "specific-groups",
  "display_groups": [{ "id": "66b0c1..." }],
  "minimum_purchase_type": "no-minimum-requirements",
  "can_be_combined": true,
  "limit_discount_usage": false,
  "limit_one_usage_per_customer": false,
  "availability": ["Online Ordering", "Kiosk", "POS"],
  "active_days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
  "active_dates": {
    "start_date": "2026-06-01",
    "start_time": "00:00",
    "set_end_date": false
  }
}

Result: 20% off any item in the selected display group(s), auto-applied, no minimum.


Buy One Get One free (BOGO)

json
{
  "location_id": "664a1f...",
  "type": "BOGO",
  "method": "Automatic",
  "name": "BOGO Burgers",
  "minimum_purchase_type": "minimum-quantity-items",
  "minimum_quantity_items": 1,
  "requirement_applies_to": "specific-groups",
  "requirement_display_groups": [{ "id": "66b0c1..." }],
  "benefit_applies_to": "specific-groups",
  "benefit_display_groups": [{ "id": "66b0c1..." }],
  "benefit_quantity": 1,
  "discount_unit": "free",
  "enable_max_usage_per_order": true,
  "max_usage_per_order": 1,
  "can_be_combined": false,
  "limit_discount_usage": false,
  "limit_one_usage_per_customer": false,
  "availability": ["Online Ordering", "Kiosk"],
  "active_days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
  "active_dates": {
    "start_date": "2026-06-01",
    "start_time": "00:00",
    "set_end_date": false
  }
}

Result: Buy 1 burger, get 1 burger free; applies at most once per order.


Buy 2, get 1 at 50% off (BOGO percentage)

json
{
  "location_id": "664a1f...",
  "type": "BOGO",
  "method": "Code",
  "discount_code": "PIZZA3",
  "name": "Pizza Deal",
  "minimum_purchase_type": "minimum-quantity-items",
  "minimum_quantity_items": 2,
  "requirement_applies_to": "specific-items",
  "requirement_items": [{ "id": "66c2d3..." }],
  "benefit_applies_to": "specific-items",
  "benefit_items": [{ "id": "66c2d3..." }],
  "benefit_quantity": 1,
  "discount_unit": "percent",
  "discount_value": 50,
  "enable_max_usage_per_order": false,
  "can_be_combined": false,
  "limit_discount_usage": true,
  "limit_discount_usage_amount": 500,
  "limit_one_usage_per_customer": false,
  "availability": ["Online Ordering"],
  "active_days": ["Friday", "Saturday", "Sunday"],
  "active_dates": {
    "start_date": "2026-07-01",
    "start_time": "12:00",
    "set_end_date": true,
    "end_date": "2026-07-31",
    "end_time": "23:59"
  }
}

Result: With code PIZZA3, buy 2 pizzas and get a 3rd at 50% off; weekends only; total of 500 redemptions, after which it computes as Expired.


Draft offer

json
{
  "location_id": "664a1f...",
  "type": "ORDER",
  "method": "Automatic",
  "name": "Draft - Flash Sale",
  "is_draft": true
}

Result: Saved as a Draft. With is_draft true, all other fields are nullable and status is forced to Draft.