Appearance
Offers & Promotions
Production gating note: The Offers back-office pages are test-environment only — a production merchant cannot reach them at all, from the menu or by URL. All four routes (
/marketing/offers,/marketing/offers/new-offer,/marketing/offers/edit/{id},/marketing/offers/duplicate/{id}) declaretestOnly: trueroute meta, andcanNavigatereturnsfalsefor any matched route carrying that meta whenever the build is not a test environment, so the router redirects to/not-authorized.isTestEnvis a build-time constant,!['production', 'staging'].includes(import.meta.env.VITE_ENV), so the gate is closed in the production and staging builds. Separately, insrc/navigation/vertical/index.tsthe Offers nav entry is pushed onto the Marketing menu only inside anif (isTestEnv)block; the code comment there names Offers explicitly alongside Gift Cards — both still over-count POS revenue on Mplus/Kassanet (gift-card redemption + monetary-discount reconciliation) and must not be exposed in production until that is fixed.testOnlyis the layer that actually blocks: it is evaluated before the abilities-hydration early return, and the sibling CASLview/offersmeta is a no-op for MERCHANT_OWNER, whose backend permission list is['*']. Only the back-office UI is gated. The/back-office/offersAPI is permission-gated, not env-gated, and offer evaluation is unchanged in production: offers that already exist keep being computed, pushed to kiosk / online ordering viagetActiveOffersForD1, and applied to customer orders. So never walk a production merchant through creating or editing an offer — but do not tell them their existing discounts have stopped working either. (Verified:upvendo-backofficesrc/pages/marketing/offers/index.vue lines 7-13, new-offer.vue lines 5-11, edit/[id].vue lines 5-11, duplicate/[id].vue lines 5-11; src/@layouts/plugins/casl.ts lines 12 and 149-167; src/plugins/1.router/guards.ts lines 33-45; src/navigation/vertical/index.ts lines 238-258;upvendo-backendapp/Constants/Roles.php lines 181-184; routes/api/backoffice/offers.php; app/RawModels/Location.php line 838.)
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. The back-office pages that do this are reachable only in a test environment — see the gating note above.
Purpose
For a test-flagged merchant, 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. In production the pages are route-blocked, so no offer can be created or edited there; offers that already exist continue to be applied to orders by the backend.
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
Every action below lives on a
testOnlyroute and is therefore available only in a test environment (see the gating note). None of them can be performed by a production merchant, regardless of role or permissions.
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}), which appears only on Active rows. (A POST .../deactivate/{id} endpoint does exist on the API and moves an Active offer to Expired, but the back office never calls it. Verified: upvendo-backend routes/api/backoffice/offers.php line 14; upvendo-backoffice src/views/marketing/offers/Offers.vue lines 511-575.) (Scheduled rows show Activate instead, and Archived rows show Unarchive.) 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.
Edit an Offer
The row-level Edit action only appears on Draft and Expired offers. Active, Scheduled and Archived offers are not editable from the list — use Duplicate instead. (Verified: upvendo-backoffice src/views/marketing/offers/Offers.vue lines 524-532.)
Duplicate an Offer
Duplicate appears on Active, Scheduled and Archived rows and opens /marketing/offers/duplicate/{id} pre-filled with the source offer's settings. (Verified: upvendo-backoffice src/views/marketing/offers/Offers.vue lines 514-522; src/pages/marketing/offers/duplicate/[id].vue.)
Delete an Offer
Permanently remove an offer and its associated item/group relations from the system. Available on any row for users with the delete-offers permission.
Location
- Backoffice Routes (all test-env only —
testOnlyroute meta blocks them in production; see gating note above):/marketing/offers,/marketing/offers/new-offer,/marketing/offers/edit/{id},/marketing/offers/duplicate/{id} - 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 intended to be active (validated against Carbon::getDays()). Stored only — not currently enforced at redemption; see the note under Offer Validation. |
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 the offer is not published to that channel
│
└── All conditions met → Apply discount
active_daysis stored but not enforced. The weekday check lives inOfferService::validateOfferDates(), which is commented out along with its call site, andactive_daysis not included in the offer payload pushed to the kiosk / online-ordering channels. An offer therefore applies on any day within its date range. (Verified:upvendo-backendapp/Services/Offer/OfferService.php lines 63-80 and 320; app/RawModels/Location.php lines 777-824 —getActiveOffersForD1()does not emitactive_days.)
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 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
The customer-facing side is not environment-gated. Active offers are serialized to D1 for the kiosk and online-ordering channels by
getActiveOffersForD1and applied by the backend with no env check, so any offer a merchant already has keeps discounting orders in production even though the back-office pages are blocked there.
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 are checked twice. The request validator rejects a code already used by another non-deleted offer at the same location (
UniqueInConnectionWithModel, scoped bylocation_id); then, when the offer is saved in a non-Draft status, the service rejects it with "The discount code is already used" if any other offer in the merchant's account holds the same code. Draft saves skip the second check. (Verified:upvendo-backendapp/Http/Requests/BackOffice/Offer/StoreOfferRequest.php lines 55-65; app/Rules/UniqueInConnectionWithModel.php lines 81-83; app/Services/BackOffice/OfferService.php lines 75-84.) - 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
"Why can't I find Offers in my back office?" Offers is test-environment only. The nav entry is wrapped in an
isTestEnvcheck and every offers route carriestestOnlymeta, so in production the pages are both hidden from the Marketing menu and blocked by the router — typing/marketing/offerslands on/not-authorized. It is gated on the build's environment, not on your role or permissions, so no permission change opens it. This holds until the Mplus/Kassanet POS revenue over-counting (gift-card redemption + monetary-discount reconciliation) is fixed. Offers that already exist on the account are unaffected and still apply to orders."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. Offer names must be unique within the location, and discount codes must be unique within the location too — plus, when you publish (save as anything other than Draft), the system also rejects a code that any other offer in your account already uses. 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: Offers page missing, or /marketing/offers redirects to Not Authorized
Cause: The nav entry is registered only when isTestEnv is true, and all four offers routes declare testOnly meta, which canNavigate rejects outside a test environment.
Solution: This is intentional and cannot be worked around by permissions or by typing the URL — the check reads the build's VITE_ENV, not the user's role. The pages are test-env only until the POS over-counting issue is resolved; it is not a misconfiguration. Existing offers keep applying to orders in the meantime.
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
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
availability(note:active_daysis not enforced, so it is never the cause)
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
Solutions:
- Confirm the method is
Automatic - Check all conditions
- Check the computed status / activate or unarchive as needed
- Verify
availability(note:active_daysis not enforced, so it is never the cause)
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, within the July date range; total of 500 redemptions, after which it computes as Expired. (active_days is recorded as Fri–Sun but is not enforced — the offer works on any day inside the date range.)
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.