Appearance
Loyalty Program
Overview
The Loyalty Program rewards customers for repeat purchases. Customers earn points (or stars, or a custom unit) on orders and unlock rewards once their balance reaches a reward's fixed threshold.
Key Purpose: Encourage repeat business through threshold-based rewards.
Purpose
This page lets you configure a loyalty program that awards points to customers based on their spending or visits, and define rewards they can unlock once they accumulate enough points.
Key Concepts
- Amount-Based Program: Customers earn points proportional to the order total, calculated as order total multiplied by the configured
points_earned_each_dollarrate (there is no subtotal option, and no per-category exclusion). - Visit-Based Program: Customers earn a fixed number of points (
points_earned_each_visit) per completed order regardless of the amount spent, optionally requiring a minimum spend threshold (using_minimum_spend/minimum_spend_to_earn_points). - Reward Redemption (threshold-based): Each reward defines a fixed
points_neededthreshold (orvisits_neededfor visit-based programs). A reward unlocks once the customer's balance reaches that threshold, and redeeming it deducts the required points. There is no point-to-currency conversion, no point value, and no maximum-redemption percentage cap. Rewards are either a Discount (percentage or fixed amount) or a Free Item with selected eligible items. - Reward Unit: The points are labelled by the configured
reward_unit— Points, Stars, or a Custom singular/plural label (e.g. "Bean"/"Beans"). - Bonus Time Multiplier: During configured bonus windows (specific days, optionally a start/end time), earned points are automatically doubled or tripled based on
specific_day_time_bonus_amount(Double Points / Triple Points). - Suspicious Activity Flag: When
program_alertis on, if a customer completes more than 3 orders at the same location in a single day (location timezone), the customer is flagged as suspicious and an alert email is sent to the merchant.
Actions
Configure Loyalty Program
Set the reward unit (Points/Stars/Custom), program type (Amount Based or Visit Based), earning rate, point expiry period (in months), sign-up bonus, birthday bonus, and day/time bonus settings via the 5-step wizard. Activating the program requires a subscription per location.
Manage Rewards
Create, update, or delete rewards that customers unlock with their points. Each reward is a Discount (percentage or fixed amount) or a Free Item with eligible item selection, and defines a points_needed (amount-based) or visits_needed (visit-based) threshold.
Manage Birthday Bonuses
Configure a birthday bonus reward per location, including discount type, value, and eligible items for free-item bonuses.
Subscribe/Unsubscribe Locations
Enable or disable the loyalty program for individual locations, which creates or cancels a Stripe subscription for the loyalty feature.
Location
- Backoffice Route:
/marketing/loyalty - Backend Controller:
app/Http/Controllers/Api/BackOffice/LoyaltyController.php - Validation:
app/Http/Requests/BackOffice/Loyalty/StoreLoyaltyRequest.php - Vue Page:
src/pages/marketing/loyalty/index.vue(renderssrc/views/marketing/loyalty/Loyalty.vue) - Create/Edit Wizard:
src/views/marketing/loyalty/LoyaltyForm.vue(5 steps undersrc/views/marketing/loyalty/components/loyaltyCreateStep/)
Fields
Fields below come from
StoreLoyaltyRequest(validation) andRawModels/Loyalty.php(persisted shape). The wizard is organized into 5 steps; the step each field belongs to is noted.
Reward Unit
| Property | Value |
|---|---|
| Field ID | reward_unit (label, singular, plural) |
| Label | What do you call your points? |
| Type | Object — label is one of Points, Stars, Custom |
| Required | reward_unit.label required. For Custom, singular and plural are also required |
| Step | 1 (Customize) |
Description: Controls how the loyalty currency is labelled. For Points and Stars the singular/plural are filled automatically; for Custom the merchant supplies their own (e.g. "Bean"/"Beans").
Program Type
| Property | Value |
|---|---|
| Field ID | program |
| Label | Program Type |
| Type | Select (enum LoyaltyPrograms) |
| Options | Amount Based, Visit Based |
| Required | Yes |
| Step | 2 (Choose Program) |
Description: Whether customers earn by amount spent or by completed visits. One type per program — not both.
Points Earned Each Dollar
| Property | Value |
|---|---|
| Field ID | points_earned_each_dollar |
| Label | Points earned per currency unit |
| Type | Number |
| Required | Required when program = Amount Based |
| Step | 2 |
Description: Points awarded per unit of order total for amount-based programs. Points earned = points_earned_each_dollar × order total (× bonus multiplier). Calculated on the order total — there is no subtotal option.
Points Earned Each Visit
| Property | Value |
|---|---|
| Field ID | points_earned_each_visit |
| Label | Points earned per visit |
| Type | Number |
| Required | Required when program = Visit Based |
| Step | 2 |
Description: Fixed points awarded for each completed order, regardless of amount, for visit-based programs.
Using Minimum Spend
| Property | Value |
|---|---|
| Field ID | using_minimum_spend |
| Label | Require a minimum spend |
| Type | Boolean |
| Required | Required when program = Visit Based (defaults false) |
| Step | 2 |
Description: When enabled (visit-based only), an order must meet the minimum spend before any points are awarded.
Minimum Spend To Earn Points
| Property | Value |
|---|---|
| Field ID | minimum_spend_to_earn_points |
| Label | Minimum spend |
| Type | Numeric (money) |
| Required | Required when using_minimum_spend is true |
| Step | 2 |
Description: The order total must be greater than or equal to this amount for visit-based points to be awarded.
Do Points Expire
| Property | Value |
|---|---|
| Field ID | do_points_expire |
| Label | Do points expire? |
| Type | Boolean |
| Required | Yes |
| Step | 2 |
Description: Master switch for point expiry. When off, points never expire.
Months To Expire Points
| Property | Value |
|---|---|
| Field ID | months_to_expire_points |
| Label | Points expire after |
| Type | Select (enum LoyaltyMonthsToExpirePoints), months |
| Options | 3, 6, 12, 18, 24 months — these are the only options |
| Required | Required when do_points_expire is true |
| Step | 2 |
Description: Number of months after earning that points expire (not days). On earn, expires_at = now + months_to_expire_points. Expired point records are marked is_expired and skipped during redemption.
Sign-Up Bonus
| Property | Value |
|---|---|
| Field ID | sign_up_bonus (boolean) + sign_up_bonus_points (number) |
| Label | Sign-up bonus |
| Type | Boolean + Number |
| Required | sign_up_bonus required; sign_up_bonus_points required when it is true |
| Step | 4 (Personalize) |
Description: One-time fixed points granted when a customer first joins (no prior customer-vendor relationship). Respects the same expiry settings as regular points.
How Customer Sign Up
| Property | Value |
|---|---|
| Field ID | how_customer_sign_up |
| Label | Sign-up method |
| Type | Select (enum LoyaltySignUpOptions) |
| Options | Email, Phone, Both |
| Required | Yes |
| Step | 4 |
Description: How customers can identify/join the program.
Birthday Bonus
| Property | Value |
|---|---|
| Field ID | birthday_bonus (boolean) + birthday_bonus_details (object) |
| Label | Birthday bonus |
| Type | Boolean + Object |
| Required | birthday_bonus required; when true, birthday_bonus_details.type (enum RewardTypes: Discount / Free Item) is required |
| Step | 4 |
Description: A birthday gift — a Discount (percentage/fixed value) or a Free Item with eligible items. It is a reward-style gift, not a fixed points grant. Stored per location as a boolean map plus a linked BirthdayBonus document.
Specific Day/Time Bonus
| Property | Value |
|---|---|
| Field ID | specific_day_time_bonus (boolean), specific_day_time_bonus_amount, specific_day_time_bonus_days[], specific_day_time_bonus_time (boolean), specific_day_time_bonus_time_start, specific_day_time_bonus_time_end |
| Label | Bonus days/times |
| Type | Boolean + select + array + time window |
| Options | specific_day_time_bonus_amount = Double Points or Triple Points (enum LoyaltyDayTimeBonusOptions); days are weekday names; times are H:i |
| Required | specific_day_time_bonus required; when true, amount, days and the time toggle are required (and start/end when the time toggle is on) |
| Step | 4 |
Description: During the selected days (and optional time window), earned points are multiplied ×2 (Double) or ×3 (Triple).
Program Alert
| Property | Value |
|---|---|
| Field ID | program_alert |
| Label | Suspicious activity alert |
| Type | Boolean |
| Required | Yes |
| Step | 4 |
Description: When on, if a customer completes more than 3 completed orders at the same location in a single day, they are flagged suspicious and the merchant is emailed.
Name & Description
| Property | Value |
|---|---|
| Field ID | name, description |
| Type | String |
| Required | Yes |
| Step | 4 |
Description: Program name and description shown to customers.
Rewards
| Property | Value |
|---|---|
| Field ID | rewards[] |
| Type | Array of reward objects |
| Required | Yes (at least one) |
| Step | 3 (Rewards) |
Each reward object (RawModels/Reward.php):
| Sub-field | Notes |
|---|---|
type | enum RewardTypes: Discount or Free Item |
name | required string |
discount_unit | enum DiscountUnits (required for Discount) |
discount_value | required for Discount; for percentage must be >0 and <=100; for amount must be >0 |
eligible_items[] | required for Free Item (item ids) |
points_needed | threshold for Amount Based programs (required, >0) |
visits_needed | threshold for Visit Based programs (required, >0); points-needed is derived from visits × points_earned_each_visit |
Description: Rewards are unlocked by reaching a fixed points_needed (amount-based) or visits_needed (visit-based) threshold. There is no point-to-currency conversion or maximum-redemption cap.
Business Logic
Points Earning
Customer places a Complete order
│
▼
Is it bonus time? (specific_day_time_bonus, day/time match)
- Yes → multiplier = 2 (Double) or 3 (Triple)
- No → multiplier = 1
│
▼
Amount Based:
points = points_earned_each_dollar × order TOTAL × multiplier
Visit Based:
if (!using_minimum_spend OR total >= minimum_spend_to_earn_points):
points = points_earned_each_visit × multiplier
(no minimum met → no points)
│
▼
If points == 0 → skip
│
▼
Save LoyaltyPoint record (with expires_at = now + months_to_expire_points
when do_points_expire is on)
│
▼
Recalculate customer lifetime points / visits / spendReward Redemption (threshold-based)
Customer selects one or more rewards at checkout
│
▼
checkIfRewardsAreRedeemable:
- For each reward, available points >= reward.points_needed?
- And available points >= SUM(all selected rewards' points_needed)?
├── No → throw "Insufficient points" (redemption blocked)
│
└── Yes → apply reward(s):
- Discount: percentage or fixed amount off eligible/all items
- Free Item: eligible item becomes free
│
▼
Deduct points across active (non-expired) LoyaltyPoint records
in order, spanning multiple records if needed
(expired records are marked is_expired and skipped)Note: redemption is threshold-based. There is NO point-to-currency value, NO minimum-redemption-points setting, and NO maximum-redemption-% cap.
Points Balance
Available points = sum of non-expired LoyaltyPoint records,
minus points already used (points_used) on each.
(For Mplus-linked customers the online balance is read from the POS
instead, falling back to the local balance if Mplus is unavailable.)Customer Impact
Online Ordering
- Account: Points balance shown in profile
- Ways to earn: Earn rate, bonus days, birthday treat shown
- Cart: "You'll earn X points on this order"
- Rewards: Each reward shows its points/visits threshold; unlocks when the balance reaches it
- Confirmation: "You earned X points"
Kiosk
- Points features may be limited
- Requires customer identification (phone/email)
- Balance shown if identified
Receipt
--- Loyalty ---
Points Earned: 25
Reward Redeemed: Free Coffee (100 points)
New Balance: 425 pointsRelations
Depends On
- Customers: Points tied to customer-merchant records
- Items: For Free Item rewards / birthday Free Item (eligible items)
- Subscription: Active Stripe subscription per location
Affects
- Online Ordering: Points display and reward redemption
- Transactions: Points earned and rewards redeemed recorded
- Mplus (MplusKassa) POS: Points/redemptions synced for linked customers
Related Features
Business Rules
- Points are only awarded on orders with a status of "Complete"; pending or cancelled orders do not earn points.
- For visit-based programs with a minimum spend enabled, the order total must meet or exceed the configured minimum spend threshold before any points are awarded.
- When redeeming rewards, the system iterates through active (non-expired) point records in order and deducts points across multiple records if needed; if total available points are insufficient, the redemption fails with an exception.
- Points expiry is set in months (not days) from the time of earning; expired point records are marked as expired and skipped during redemption.
- The loyalty program requires an active Stripe subscription per location; the
getLoyaltyButOnlyIfSubscribedmethod returns null if the location is not subscribed, effectively disabling all loyalty features for that location.
FAQs
"Can a customer use both amount-based and visit-based programs?" No, each loyalty program is configured as either amount-based or visit-based, not both simultaneously.
"What happens to points when an order is refunded?" Points are awarded only on completed orders; however, the system does not automatically reverse points if an order is later refunded -- manual adjustment may be needed.
"How does the sign-up bonus work?" When a new customer-vendor record is created (i.e., the customer has no prior relationship with this vendor), the system automatically awards the configured sign-up bonus points, respecting the same expiry settings as regular points.
"Can rewards be combined in a single order?" Yes, multiple reward IDs can be submitted in a single order, but the system validates that the customer has enough total available points to cover all selected rewards combined.
"What triggers the suspicious activity alert?" If a customer has more than 3 completed orders at the same location within a single calendar day (in the location's timezone), the customer is flagged and the merchant receives an alert email.
"How do I set up a loyalty program — what are the steps?" Go to Marketing → Loyalty and follow the 5-step wizard: Customize (choose your reward unit), Choose Program (Amount Based or Visit Based and earn rate), Rewards, Personalize (sign-up/birthday/bonus-time options), then Review & Create.
"What's the difference between an Amount Based and a Visit Based program?" Amount Based awards points based on how much is spent (points per €1), while Visit Based awards a fixed number of points for every completed visit regardless of spend. You pick one type per program — not both.
"Can I require customers to spend a minimum before they earn points?" Yes, but only on Visit Based programs. Enable "minimum spend" and set the amount; the order total must meet or exceed it for any points to be awarded.
"What can I name the points — do they have to be called 'points'?" No. In step 1 you choose a reward unit: Points, Stars, or a Custom unit. If you pick Custom, you enter your own singular and plural label (e.g. "Bean"/"Beans").
"How long until points expire, and what options do I have?" Expiry is optional. If you turn on "Do points expire," you choose a fixed period of 3, 6, 12, 18, or 24 months — those are the only options. If expiry is off, points never expire.
"How do customers redeem points for a reward?" You define rewards, each with a points (or visits) threshold. A reward unlocks once the customer's balance reaches that threshold, and redeeming it deducts the required points. Rewards can be a Discount (percentage or fixed amount) or a Free Item with selected eligible items.
"Can a customer redeem more than one reward in the same order?" Yes. Multiple rewards can be applied to one order, but the system checks the customer has enough total available points to cover them all combined.
"How do I offer double or triple points on certain days?" In Personalize, turn on the day/time bonus and choose Double or Triple Points, then pick the days (and optionally a start/end time). Points earned in those windows are automatically multiplied.
"What do the sign-up and birthday bonuses do, and how do customers join?" You can grant a one-time sign-up bonus (fixed points when a customer first joins) and a birthday bonus (a Discount or Free Item gift). The sign-up method is set to Email, Phone, or Both.
"Why isn't a customer earning points?" Points are only awarded on orders that reach "Complete" status, and only when the location has an active loyalty subscription. For Visit Based programs with minimum spend, the order must also meet the minimum.
"Do I have to pay to turn on loyalty, and is it per location?" Yes. Activating the program creates a Stripe subscription per location. If a location isn't subscribed, loyalty features are disabled there — points won't be earned or shown.
"I use Mplus (MplusKassa) POS — will loyalty points stay in sync?" Yes. For Mplus-linked customers, earned points and redemptions are pushed to the Mplus relation, and the online balance is read from the POS. The sync is non-fatal — if Mplus is unavailable it falls back to the local balance rather than blocking the order.
Troubleshooting
Problem: Customer not earning points
Causes:
- Location has no active loyalty subscription
- Customer not identified / not logged in
- Visit-based program with minimum spend not met
- Order not in "Complete" status
Solutions:
- Confirm the location is subscribed to loyalty
- Ensure the customer is identified (email/phone per sign-up method)
- Check the minimum spend setting and the order total
- Verify order status is Complete
Problem: Can't redeem a reward
Causes:
- Balance below the reward's
points_needed/visits_neededthreshold - Multiple rewards selected exceed total available points
- Points expired (records marked is_expired)
- Mplus balance stale / POS unavailable
Solutions:
- Check the reward's threshold versus the customer's balance
- Reduce the number of rewards applied to one order
- Check expiry settings and earned dates
- For Mplus-linked customers, the balance is refreshed before redemption; verify the POS integration
Problem: Points balance incorrect
Causes:
- Points expired
- Redemption not recorded
- Order cancelled/refunded
- Mplus sync issue (POS balance overrides local)
Solutions:
- Check expiry settings and dates
- Review transaction history
- Check for refunded orders (points are not auto-reversed)
- Verify the Mplus integration / cached balance
Problem: Points expiring too fast
Causes:
- Short expiry period configured (3 or 6 months)
- Customer inactive
Solutions:
- Increase the expiry period (up to 24 months) or turn expiry off
- Send reminder communications before expiry
Examples
JSON below reflects the real
StoreLoyaltyRequestpayload shape.
Simple Amount-Based Program
json
{
"reward_unit": { "label": "Points", "singular": "Point", "plural": "Points" },
"program": "Amount Based",
"points_earned_each_dollar": 1,
"do_points_expire": false,
"rewards": [
{
"type": "Discount",
"name": "€1 off",
"discount_unit": "percent",
"discount_value": 10,
"points_needed": 100
}
],
"name": "Rewards Club",
"description": "Earn points on every order",
"how_customer_sign_up": "Email",
"sign_up_bonus": true,
"sign_up_bonus_points": 50,
"birthday_bonus": false,
"specific_day_time_bonus": false,
"program_alert": true
}How it works:
- Earn 1 point per currency unit of the order total
- A reward unlocks at 100 points (a 10% discount)
- Points never expire
- 50-point sign-up bonus
Visit-Based Program with Minimum Spend
json
{
"reward_unit": { "label": "Stars", "singular": "Star", "plural": "Stars" },
"program": "Visit Based",
"points_earned_each_visit": 1,
"using_minimum_spend": true,
"minimum_spend_to_earn_points": 10,
"do_points_expire": true,
"months_to_expire_points": 12,
"rewards": [
{
"type": "Free Item",
"name": "Free Coffee",
"eligible_items": [{ "id": "<item_id>" }],
"visits_needed": 10
}
],
"name": "Coffee Card",
"description": "Collect a star each visit",
"how_customer_sign_up": "Both",
"sign_up_bonus": false,
"birthday_bonus": true,
"birthday_bonus_details": {
"type": "Free Item",
"eligible_items": [{ "id": "<item_id>" }]
},
"specific_day_time_bonus": false,
"program_alert": true
}How it works:
- Earn 1 star per visit, but only when the order total is at least 10
- 10 stars unlocks a free coffee
- Stars expire after 12 months
- Birthday treat is a free item
Amount-Based with Double-Points Days
json
{
"reward_unit": { "label": "Custom", "singular": "Bean", "plural": "Beans" },
"program": "Amount Based",
"points_earned_each_dollar": 2,
"do_points_expire": true,
"months_to_expire_points": 6,
"rewards": [
{
"type": "Discount",
"name": "€5 off",
"discount_unit": "amount",
"discount_value": 5,
"points_needed": 200
}
],
"name": "Bean Club",
"description": "Earn beans, double on weekends",
"how_customer_sign_up": "Phone",
"sign_up_bonus": true,
"sign_up_bonus_points": 100,
"birthday_bonus": false,
"specific_day_time_bonus": true,
"specific_day_time_bonus_amount": "Double Points",
"specific_day_time_bonus_days": ["Saturday", "Sunday"],
"specific_day_time_bonus_time": false,
"program_alert": true
}How it works:
- Earn 2 beans per currency unit; doubled to 4 on Saturdays and Sundays
- 200 beans unlocks a €5-off discount
- Beans expire after 6 months
- Custom "Bean"/"Beans" labelling, 100-bean sign-up bonus