Skip to content

Coupons

Overview

A coupon is an Offer with method "Code". Customers must know and enter the code at checkout to receive the discount. Unlike an Automatic offer (which applies on its own when its conditions are met), a Code offer only applies when the customer enters its discount code. Coupons are not a separate feature — they are created and managed under Marketing → Offers.

Key Purpose: Create shareable discount codes for marketing campaigns.

Purpose

This page (the Offers page) lets you create and manage offers, including code-based offers (coupons) that customers enter at checkout. A coupon provides a percentage or fixed-amount discount on qualifying items or on the order total, subject to minimum-purchase, usage, scheduling, and channel rules.

Key Concepts

  • Coupon as Offer: A coupon is an Offer (method = 'Code'). It shares the same model, validation, redemption tracking, and discount calculation as Automatic offers; the only difference is that it requires the customer to enter a discount_code.
  • Discount Code: A unique string tied to a Code offer (discount_code); on publish the backend validates uniqueness across offers at the location to prevent duplicate codes.
  • Limit One Usage Per Customer: When limit_one_usage_per_customer is enabled, the system checks that customer's prior redemption records for the offer before allowing another redemption.
  • Usage Limit: When limit_discount_usage is enabled, limit_discount_usage_amount caps total redemptions. Each successful use writes a redemption record and increments the offer's redemptions counter; once the cap is reached the offer becomes Expired and the code stops working.

Actions

Create a Coupon

Create a new offer, set its method to Code, and define the discount code, offer type (Amount of items, Amount of order, or Buy X Get Y), discount unit and value, minimum-purchase requirements, usage limits, availability channels, and start/end schedule.

Edit a Coupon

Update an existing offer's discount value, minimum-purchase, usage limits, applicable items/groups, availability, or schedule. Draft offers can be freely edited; once an offer leaves Draft it can be updated but cannot revert to Draft.

Deactivate a Coupon

Deactivate or archive an offer so it can no longer be redeemed. An offer past its usage cap or end date becomes Expired automatically.

Location

  • Backoffice Route: /marketing/offers
  • Backend Request: app/Http/Requests/BackOffice/Offer/StoreOfferRequest.php
  • Backend Controller: app/Http/Controllers/Api/OfferController.php
  • Backend Service: app/Services/Offer/OfferService.php
  • Vue Components: src/views/marketing/offers/Offers.vue, src/views/marketing/offers/CreateOfferForm.vue, src/views/marketing/offers/components/create-offers/OfferInformation.vue

Fields

Method

PropertyValue
Field IDmethod
LabelMethod
TypeTab / Select
OptionsAutomatic, Code
RequiredYes

Description: How the offer is triggered. Code makes the offer a coupon (the customer must enter the discount code); Automatic applies on its own when conditions are met. Switching to a different method clears the discount code.


Discount Code

PropertyValue
Field IDdiscount_code
LabelDiscount Code
TypeText
RequiredYes (only when method = 'Code')
ValidationUnique across non-deleted offers at the location (request validator) and, on publish, across every offer in the merchant's account; max 10 characters (front-end)

Description: The code customers enter at checkout. The "Generate" button produces a random 8-character code using A–Z and 0–9.

Best Practices:

  • Keep codes short and memorable
  • Avoid confusing characters (0/O, 1/I)
  • Make codes relevant to campaign

Examples:

  • "SUMMER20"
  • "WELCOME10"
  • "PIZZA50"

Name

PropertyValue
Field IDname
LabelName
TypeText
RequiredYes
ValidationUnique across offers at the location; max 25 characters (front-end)

Description: Internal name for the offer.

Examples:

  • "Summer Campaign"
  • "Influencer Code"
  • "Newsletter Discount"

Offer Type

PropertyValue
Field IDtype
LabelOffer Type
TypeSelect
OptionsITEMS (Amount of items), ORDER (Amount of order), BOGO (Buy X Get Y)
RequiredYes

Description: What the offer discounts. "Amount of items" discounts specific items/groups, "Amount of order" discounts the order total, and "Buy X Get Y" applies a benefit when a requirement is met.


Discount Unit

PropertyValue
Field IDdiscount_unit
LabelDiscount Unit
TypeSelect
Optionspercent, amount (for ITEMS/ORDER); percent, amount, free for BOGO
RequiredYes

Description: Whether the discount is a percentage or a fixed amount. The free unit exists only for Buy X Get Y offers.


Discount Value

PropertyValue
Field IDdiscount_value
LabelDiscount Value
TypeNumber
RequiredYes (for ITEMS/ORDER, and BOGO percent/amount)
Validationpercent: > 0 and <= 100; amount: > 0

Description: Amount of discount.

Examples:

  • Percent: 20 (20% off)
  • Amount: 5 (€5 off)

Offer Applies To

PropertyValue
Field IDoffer_applies_to
LabelApplies To
TypeSelect
Optionsspecific-groups, specific-items
RequiredYes (for ITEMS type)

Description: Whether an "Amount of items" offer applies to selected display groups (display_groups) or selected items (items). For BOGO, the equivalents are requirement_applies_to / benefit_applies_to.


Minimum Purchase Type

PropertyValue
Field IDminimum_purchase_type
LabelMinimum Purchase Requirements
TypeSelect
Optionsno-minimum-requirements, minimum-purchase-amount, minimum-quantity-items
RequiredYes

Description: The threshold a cart must meet. minimum-purchase-amount requires minimum_purchase_amount (> 0); minimum-quantity-items requires minimum_quantity_items (> 0). BOGO offers do not allow "no minimum requirements".


Limit Discount Usage

PropertyValue
Field IDlimit_discount_usage
LabelLimit discount usage
TypeBoolean
RequiredYes

Description: When enabled, limit_discount_usage_amount (> 0) caps total redemptions across all customers. Once reached, the offer becomes Expired.


Limit One Usage Per Customer

PropertyValue
Field IDlimit_one_usage_per_customer
LabelLimit one usage per customer
TypeBoolean
RequiredYes

Description: When enabled, each customer may redeem the offer only once.


Can Be Combined

PropertyValue
Field IDcan_be_combined
LabelCan be combined
TypeBoolean
RequiredYes

Description: Whether this offer can stack with other offers.


Availability

PropertyValue
Field IDavailability
LabelAvailability (channels)
TypeMulti-select (array)
OptionsKiosk, POS, Online Ordering, Table Qr Ordering, Uber Eats, Takeaway, Shopify
RequiredYes

Description: The sales channels on which the offer (code) is valid.


Active Days

PropertyValue
Field IDactive_days
LabelActive days
TypeMulti-select (array)
OptionsSunday–Saturday
RequiredYes

Description: Days of the week the offer is intended to be active. Stored but not enforced — the day-of-week check is commented out in OfferService and active_days is not sent to the kiosk / online-ordering channels, so the coupon works on any day inside its date range.


Active Dates

PropertyValue
Field IDactive_dates
LabelSchedule
TypeObject
RequiredYes

Description: Scheduling fields: active_dates.start_date (Y-m-d), active_dates.start_time (H:i), active_dates.set_end_date (boolean) and, when set, active_dates.end_date / active_dates.end_time. Before the start the status is Scheduled; after the end it becomes Expired.


Status

PropertyValue
Field IDstatus
LabelStatus
TypeSelect (computed)
OptionsDraft, Active, Scheduled, Expired, Archived

Description: The offer's lifecycle status. Drafts are set via is_draft. Draft and Archived are sticky — getCurrentStatus() returns them unchanged; every other status is computed from the schedule and redemption cap (Scheduled before start, Expired past end or once the usage cap is reached, otherwise Active). Once an offer leaves Draft it cannot return to Draft. (Verified: upvendo-backend app/RawModels/Offer.php lines 225-292; app/Services/BackOffice/OfferService.php lines 176-178.)


Business Logic

Coupon Validation

Customer enters discount code


Find offer by discount_code (case-insensitive)
├── Not found → "Invalid promo code"

└── Found → Validate:
    ├── Stored status is Expired? Yes → "Offer <name> is not active"
    ├── can_be_combined respected? No → "Offer <name> cannot be combined with other offers"
    ├── limit_discount_usage reached? Yes → "Offer <name> max redemption has been met"
    ├── limit_one_usage_per_customer already used? Yes → "Offer <name> has already been redeemed by this customer"
    ├── Minimum purchase met? No → "Offer <name> requires a minimum order amount of X in total"
    └── All valid → Apply discount

The schedule is enforced indirectly: only offers whose computed status is Active are published to the kiosk / online-ordering channels, so a Scheduled or Expired coupon is never offered in the first place. There is no date check inside the redemption path — OfferService::validateOfferDates() and its call site are both commented out, so active_days (day-of-week) is stored but never enforced. (Verified: upvendo-backend app/Services/Offer/OfferService.php lines 56-80 and 316-327; app/RawModels/Location.php lines 777-824.)

Discount Calculation

Percent unit:
  Discount = base × (discount_value / 100)   (discount_value ≤ 100)

Amount unit:
  Discount = discount_value
  Cannot exceed the base it applies to

Where "base" is the eligible items/groups (ITEMS),
the order total (ORDER), or the benefit items (BOGO).

Customer Impact

Checkout Flow

  1. Customer enters coupon code
  2. System validates code
  3. If valid → Discount applied, shown in summary
  4. If invalid → Error message displayed

Error Messages

Online-ordering storefront (code entry, checked client-side against the offers published to the channel):

ScenarioMessage
Code doesn't match any published Code offer"Invalid promo code"
Offer found but its requirements aren't met"Requirements not met"
Offer already on the order"This offer is already applied"
Non-combinable offer"This offer cannot be combined with other offers"
A discount loyalty reward is already applied and the code is an ORDER-type (whole-cart) offer"Cannot apply offers when a discount reward is active" — only ORDER-type code offers are blocked; ITEMS and BOGO coupons can still be entered while a discount reward is active

(Verified: zestidoo-online-ordering src/stores/offer.ts lines 534-561; src/plugins/i18n/locales/en.ts lines 945-982.)

Backend, when the order is submitted (Offer <name> is the offer's own name):

ScenarioMessage
Offer is Expired"Offer <name> is not active"
Non-combinable offer sent with others"Offer <name> cannot be combined with other offers"
Total usage cap reached"Offer <name> max redemption has been met"
Per-customer limit already used"Offer <name> has already been redeemed by this customer"
Minimum quantity not met"Offer <name> requires at least <n> items in total"
Minimum amount not met"Offer <name> requires a minimum order amount of <x> in total"

(Verified: upvendo-backend app/Services/Offer/OfferService.php lines 56-102 and 160-180.)


Relations

Depends On

  • Customers: For per-customer usage tracking
  • Display Groups/Items: For "Amount of items" and Buy X Get Y targeting

Affects

  • Transactions: Discount applied
  • Reports: Coupon usage analytics

Business Rules

  • Discount codes are checked twice: the request validator rejects a code already used by another non-deleted offer at the same location, and on publish (any non-Draft save) the service additionally rejects a code used by any other offer in the merchant's account with "The discount code is already used". Draft saves skip the second check. (Verified: upvendo-backend app/Http/Requests/BackOffice/Offer/StoreOfferRequest.php lines 55-65; app/Rules/UniqueInConnectionWithModel.php lines 81-83; app/Services/BackOffice/OfferService.php lines 75-84.)
  • Before applying a discount the system validates the stored status, the combination rule (can_be_combined), the total usage limit (limit_discount_usage_amount), per-customer usage (limit_one_usage_per_customer), and minimum-purchase requirements. Start/end dates are handled by the status computation (only Active offers are published to a channel), and the day-of-week list (active_days) is not enforced at all. (Verified: upvendo-backend app/Services/Offer/OfferService.php lines 56-80 and 316-327.)
  • For a percentage discount, discount_value must be greater than 0 and at most 100; for an amount discount it must be greater than 0.
  • Redemptions are tracked per customer; each successful use creates an offer redemption record and increments the offer's redemptions counter. Once limit_discount_usage_amount is reached the offer becomes Expired.
  • An offer in any non-Draft status cannot be reverted to Draft; once published, it can only be deactivated, expired, or archived.

FAQs

  • "Can a coupon be used together with another offer?" Only if the offers allow it via the can_be_combined flag; otherwise the system rejects the combination.

  • "What happens if a coupon code is entered incorrectly?" The storefront matches the entered code against the Code offers published to that channel, ignoring case; if nothing matches it shows "Invalid promo code".

  • "How is the per-customer limit tracked?" When limit_one_usage_per_customer is enabled, the system checks that customer's prior redemption records for the offer; if one exists, further redemptions are blocked.

  • "Can I reuse a coupon code after removing the old coupon?" Yes — once the old offer is removed, its discount code is no longer matched and can be assigned to a new offer.

  • "Where do I create a coupon code in the back office?" Coupons live inside Offers, not a separate page. Go to Marketing → Offers, create a new offer, and on the Information step set the method to Code instead of Automatic; that turns the offer into a code customers must enter.

  • "What's the difference between a coupon and an offer?" They're the same underlying object. An offer with method "Automatic" applies on its own when conditions are met; an offer with method "Code" requires the customer to enter the code (a coupon). All other settings and redemption tracking are identical.

  • "How long can my coupon code be?" The discount code field accepts up to 10 characters. The Generate button produces a random 8-character code using A-Z and 0-9.

  • "What discount types can a coupon give?" You set a discount unit of percentage or fixed amount. For percentage, the value must be greater than 0 and at most 100; for fixed amount it must be greater than 0. (A "free" unit exists only for Buy X Get Y offers, not as a general coupon discount.)

  • "What kinds of discounts can a coupon apply to?" Choose the offer type: Amount of items (specific items/groups), Amount of order (the order total), or Buy X Get Y. The type determines which items the code discounts.

  • "Can I limit how many times a coupon is used overall?" Yes. Enable "limit discount usage" and set the amount. Once total redemptions reach that number, the offer auto-expires and the code stops working.

  • "Can I stop the same customer from using a coupon more than once?" Yes. Turn on "limit one usage per customer." The system checks that customer's redemption records and blocks a repeat use.

  • "Can I set when a coupon starts and stops working?" Yes. Each offer has a start date/time and an optional end date/time. Before the start it shows Scheduled; after the end it becomes Expired. The form also collects a list of active days of the week, but that list is not currently enforced — the coupon works on any day inside its date range.

  • "Which sales channels can a coupon apply to?" You pick channels under availability: Kiosk, POS, Online Ordering, Table QR Ordering, Uber Eats, Takeaway, and Shopify. The code only works on the channels you select.

  • "Can a coupon be combined with other offers?" Each offer has a single "can be combined" setting. Enable it to let the code stack with other offers; otherwise it applies on its own.

  • "How do I require a minimum spend before a coupon works?" Set the minimum purchase type to "minimum purchase amount" and enter the amount, or "minimum quantity items" and enter the count. There's also a "no minimum requirements" option.

  • "Why does saving my coupon say 'The discount code is already used'?" Discount codes must be unique. On publish, the system checks every offer in your account for the same code (the form itself already blocks duplicates within the location); if another offer uses it, pick a different code. Draft saves skip the publish-time check.

  • "Can I reuse a code after I'm done with an old coupon?" Yes. Once the old offer is removed, its code is no longer matched, so the same code can be assigned to a new offer.

  • "How do I turn off a coupon immediately?" Set its status so it's no longer active (Expired/Archived). An expired coupon can no longer be redeemed. Note that once an offer leaves Draft it can't be moved back to Draft.

Troubleshooting

Problem: Coupon code not working

Causes:

  1. Code entered incorrectly
  2. Offer expired or not yet started (start/end schedule)
  3. Usage limit reached (limit_discount_usage_amount)
  4. Minimum purchase not met
  5. Offer not active, or channel not selected in availability

Solutions:

  1. Check exact code spelling
  2. Verify the start/end schedule (the active_days list is not enforced, so it is never the cause)
  3. Check the redemption count vs the usage cap
  4. Add more items / meet the minimum purchase
  5. Activate the offer and confirm the channel is enabled

Problem: Discount amount wrong

Causes:

  1. Only some items/groups are eligible (offer_applies_to)
  2. Percentage vs amount unit
  3. Discount applies to the wrong base (items vs order)

Solutions:

  1. Review the targeted items/display groups
  2. Verify the discount unit and value
  3. Confirm the offer type matches the intended base

Examples

Welcome Discount (one per customer, on the order total)

json
{
  "method": "Code",
  "discount_code": "WELCOME15",
  "name": "Welcome",
  "type": "ORDER",
  "discount_unit": "percent",
  "discount_value": 15,
  "minimum_purchase_type": "minimum-purchase-amount",
  "minimum_purchase_amount": 20,
  "limit_discount_usage": false,
  "limit_one_usage_per_customer": true,
  "can_be_combined": false
}

Flash Sale (capped total usage, scheduled window)

json
{
  "method": "Code",
  "discount_code": "FLASH30",
  "name": "Flash Sale",
  "type": "ORDER",
  "discount_unit": "percent",
  "discount_value": 30,
  "minimum_purchase_type": "minimum-purchase-amount",
  "minimum_purchase_amount": 30,
  "limit_discount_usage": true,
  "limit_discount_usage_amount": 100,
  "limit_one_usage_per_customer": true,
  "can_be_combined": false,
  "active_dates": {
    "start_date": "2024-01-15",
    "start_time": "12:00",
    "set_end_date": true,
    "end_date": "2024-01-15",
    "end_time": "14:00"
  }
}

Item-Group Specific (half off pizzas)

json
{
  "method": "Code",
  "discount_code": "PIZZA50",
  "name": "Half Off Pizzas",
  "type": "ITEMS",
  "discount_unit": "percent",
  "discount_value": 50,
  "offer_applies_to": "specific-groups",
  "display_groups": [{ "id": "<pizzas-group-id>" }],
  "minimum_purchase_type": "no-minimum-requirements",
  "limit_discount_usage": false,
  "limit_one_usage_per_customer": false,
  "can_be_combined": false
}

Fixed Amount Off Order

json
{
  "method": "Code",
  "discount_code": "SAVE5",
  "name": "€5 Off",
  "type": "ORDER",
  "discount_unit": "amount",
  "discount_value": 5,
  "minimum_purchase_type": "minimum-purchase-amount",
  "minimum_purchase_amount": 15,
  "limit_discount_usage": false,
  "limit_one_usage_per_customer": false,
  "can_be_combined": false
}