Appearance
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, enumOfferTypes): Three types exist --ITEMS(discount on specific items or display groups),ORDER(discount on the whole cart), andBOGO(buy X items, get Y items discounted or free). - Offer Methods (
method, enumOfferMethods): An offer is eitherAutomatic(auto-applied when conditions are met) orCode(requires the customer to enter a unique discount code at checkout). Thediscount_codefield is only collected for the Code method. - Offer Statuses (
status, enumOfferStatuses): Offers follow a lifecycle ofDraft,Scheduled(enum caseUPCOMING),Active,Expired, andArchived. 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, enumOfferMinimumPurchaseTypes): Offers can requireno-minimum-requirements, aminimum-quantity-items(withminimum_quantity_items), or aminimum-purchase-amount(withminimum_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 viasrc/pages/marketing/offers/index.vue) - Create/Edit Form:
src/views/marketing/offers/CreateOfferForm.vue(with step components undercomponents/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
| Property | Value |
|---|---|
| Field ID | name |
| Type | Text (string) |
| Required | Yes |
| Validation | Required, 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
| Property | Value |
|---|---|
| Field ID | type |
| Type | Select (enum OfferTypes) |
| Required | Yes |
| Options | ITEMS (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
| Property | Value |
|---|---|
| Field ID | method |
| Type | Select (enum OfferMethods) |
| Required | Yes |
| Options | Automatic, Code |
Description: Automatic applies when conditions are met; Code requires the customer to enter a discount code at checkout.
Discount Code
| Property | Value |
|---|---|
| Field ID | discount_code |
| Type | Text (string) |
| Required | Required only when method = Code |
| Validation | Max 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
| Property | Value |
|---|---|
| Field ID | discount_unit |
| Type | Select |
| Required | Yes (for ITEMS, ORDER, and BOGO) |
| Options | ITEMS / 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
| Property | Value |
|---|---|
| Field ID | discount_value |
| Type | Number |
| Required | Required when discount_unit is percent or amount (not used for free) |
| Validation | percent: 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)
| Property | Value |
|---|---|
| Field ID | offer_applies_to |
| Type | Select (enum OfferAppliesTo) |
| Required | Required for ITEMS offers |
| Options | specific-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
| Property | Value |
|---|---|
| Field ID | minimum_purchase_type |
| Type | Select (enum OfferMinimumPurchaseTypes) |
| Required | Yes (for ITEMS and ORDER any of three; BOGO restricted to the two non-empty options) |
| Options | no-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
| Property | Value |
|---|---|
| Field ID | can_be_combined |
| Type | Boolean |
| Required | Yes |
Description: If false, the offer cannot be applied alongside other offers in the same order.
Usage Limits
| Field ID | Type | Required | Description |
|---|---|---|---|
limit_discount_usage | Boolean | Yes | Enable a total redemption cap across all customers. |
limit_discount_usage_amount | Number (> 0) | Required when limit_discount_usage is true | The total redemption cap. When reached, the offer is computed as Expired. |
limit_one_usage_per_customer | Boolean | Yes | If true, a customer who already redeemed the offer is blocked from redeeming it again. |
Availability & Schedule
| Field ID | Type | Required | Description |
|---|---|---|---|
availability | Array of enum ChannelOptions | Yes | Channels the offer is available on: Kiosk, POS, Online Ordering, Table Qr Ordering, Uber Eats, Takeaway, Shopify. |
active_days | Array of weekday names | Yes | Days of week the offer is active (validated against Carbon::getDays()). |
active_dates.start_date | Date Y-m-d | Yes | Start date. |
active_dates.start_time | Time H:i | Yes | Start time. |
active_dates.set_end_date | Boolean | Yes | Whether the offer has an end date. |
active_dates.end_date | Date Y-m-d | Required when set_end_date is true | End date. |
active_dates.end_time | Time H:i | Required when set_end_date is true | End time. |
Status & Draft
| Field ID | Type | Required | Description |
|---|---|---|---|
is_draft | Boolean | No | When true, the offer is saved as a Draft and all other fields become nullable. |
status | String (enum OfferStatuses) | Set to Draft automatically when is_draft is true | Otherwise the effective status is computed by getCurrentStatus(). |
location_id | String | Yes | The location the offer belongs to. |
BOGO (Buy X Get Y) Fields
These fields apply only when type = BOGO.
Customer Buys (requirement)
| Field ID | Type | Required | Description |
|---|---|---|---|
requirement_applies_to | Enum OfferAppliesTo (specific-groups / specific-items) | Yes | What the customer must buy to qualify. |
requirement_display_groups | Array of { id } | Required when requirement_applies_to = specific-groups | Qualifying display groups. |
requirement_items | Array of { id } | Required when requirement_applies_to = specific-items | Qualifying items. |
The minimum (quantity of items or purchase amount) is set via minimum_purchase_type as described above.
Customer Gets (benefit)
| Field ID | Type | Required | Description |
|---|---|---|---|
benefit_quantity | Number (> 0) | Yes | How many benefit items the customer gets. |
benefit_applies_to | Enum OfferAppliesTo (specific-groups / specific-items) | Yes | What the benefit applies to. |
benefit_display_groups | Array of { id } | Required when benefit_applies_to = specific-groups | Benefit display groups (stored as benefit_display_group_ids). |
benefit_items | Array of { id } | Required when benefit_applies_to = specific-items | Benefit items (stored as benefit_item_ids). |
discount_unit | Enum DiscountUnitsExpanded (percent / amount / free) | Yes | Discount applied to the benefit items. |
enable_max_usage_per_order | Boolean | Yes | Whether to cap how many times the deal applies per order. |
max_usage_per_order | Number (> 0) | Required when enable_max_usage_per_order is true | The 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 discountDiscount 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 orderCombination 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 rejectedCustomer Impact
Online Ordering
- Discount Code Entry: Field at checkout (for
Codeoffers) - Automatic: Discount shown automatically (for
Automaticoffers) - Available Offers: List of applicable offers
- Validation: Error messages for invalid codes
- Discount Display: Discount line in order summary
Kiosk
- Discount code entry (for
Codeoffers) - Automatic offers applied when conditions are met
- Discount shown on screen
Receipt
Subtotal: €25.00
Discount (SUMMER20): -€5.00
Total: €20.00Relations
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
Related Features
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:
- Code entered incorrectly
- Offer expired (past end date or total usage limit reached)
- Offer not yet started (Scheduled)
- Minimum purchase requirement not met
- Qualifying items/groups not in cart
- Channel not in the offer's
availability, or not active on today's weekday
Solutions:
- Check exact code spelling
- Verify the active date range and total usage limit (
limit_discount_usage_amount) - Verify the start date/time
- Meet the
minimum_purchase_typerequirement - Add qualifying items/groups to the cart
- Verify
availabilityandactive_days
Problem: Automatic offer not applying
Causes:
- Method is
Code, notAutomatic - Conditions not met (minimum purchase, qualifying items/groups)
- Offer status is not Active (Draft, Scheduled, Expired, or Archived)
- Channel not in
availability, or wrong weekday inactive_days
Solutions:
- Confirm the method is
Automatic - Check all conditions
- Check the computed status / activate or unarchive as needed
- Verify
availabilityandactive_days
Problem: Discount amount wrong
Causes:
- Only some items/groups are eligible
- ORDER discount spread proportionally across cart items
- Percentage vs fixed amount confusion
Solutions:
- Verify
offer_applies_toand the selected items/groups - For ORDER offers, the discount is split across all items by share of total
- Confirm
discount_unitanddiscount_value
Problem: Customer used offer multiple times
Causes:
limit_one_usage_per_customernot enabled- Different accounts used
- Guest checkout (no tracking)
Solutions:
- Enable
limit_one_usage_per_customer - Require account for offers
- 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.