Workflow: Loyalty Program — Customer Loyalty
Modules involved: Loyalty (Rules · Transactions · Redemption · Membership · Programs) · Invoices · OmniSales · Clients
Used by: Marketing · Sales · Customer Success
Typical duration: Continuous — points accumulate with each payment
Overview
The Loyalty module implements a complete loyalty system: automatic point accumulation upon invoice payment, three types of bonus awards (new account, birthday, manual), redemption in 4 different contexts (admin invoice, client portal, OmniSales, voucher), and a tier system with differentiated benefits per tier.
Required condition: The global toggle
loyalty_settingmust be set to1in Setup → Marketing → Loyalty. Without this, theafter_payment_addedhook executes no points logic regardless of configured rules.
There is no automatic point expiration. Points decrease only through explicit redemption.
Flow diagram
[SETUP — admin, one time]
│
├── Global activation: loyalty_setting = 1
├── Loyalty Rules: earn type (card_total / product_category / product)
│ + redemption tiers (point_weight per point band)
├── Membership Rules (Tiers): Bronze / Silver / Gold by point range
├── Membership Programs: discounts per tier (card/category/product)
└── Loyalty Cards: visual template (optional)
│
↓
[POINT ACCUMULATION — automatic on payment]
│
├── Trigger: after_payment_added → invoice status = PAID (2)
├── System searches for active rules for the client (by client_id or client_group)
│
├── rule_base = 'card_total':
│ points = poin_awarded × floor(invoice_total / purchase_value)
│ (e.g.: 10 points per 100 RON spent)
│
├── rule_base = 'product_category':
│ per invoice line: if product group = rel_id in rule_detail
│ → points = loyalty_point × quantity
│
└── rule_base = 'product':
per invoice line: if product = rel_id in rule_detail
→ points = loyalty_point × quantity
│
├── Insert into loy_transation: type='debit', reference='order_debit'
└── clients.loy_point += points earned
│
↓
[BONUS AWARDS]
│
├── New account (after_client_added hook):
│ → rule with MAX(create_account_point) active today
│ → type='credit', reference='manual_credit'
│
├── Birthday (cron before_cron_run hook):
│ → finds clients with custom field 'Birthday' = today (month+day)
│ → once per year (checks loy_transation.note='bonus_points_for_customers_birthday')
│ → sends email template 'loyalty-birthday-bonus-point'
│
└── Manual (admin): loyalty/transation_form → type='credit'
│
↓
[TIER — calculated dynamically]
│
├── client_rank() → finds loy_mbs_rule where loy_point ∈ [loyalty_point_from, loyalty_point_to]
├── Highest matching tier awarded
└── client_next_rank() → shows how many points are needed for the next tier
│
↓
[REDEMPTION — 4 contexts]
│
├── [A] Admin on invoice: redeem field in the invoice form
├── [B] Client in portal: invoice page → redeem points
├── [C] OmniSales/POS: redeem field in the order cart
└── [D] Voucher/Membership program: voucher code or automatic discount per tier
│
↓
[DISCOUNT APPLIED]
│
├── Points converted to currency via point_weight (e.g.: 1 pt = 0.05 RON)
├── redeemp_type='full': all points, value calculated automatically
├── redeemp_type='partial': client chooses how many points to redeem
├── Insert into loy_redeem_log: old_point, new_point, redeep_from, redeep_to
└── clients.loy_point -= points redeemed
[LOYAL CLIENT ✓ · POINTS ACCUMULATED ✓]
Step by step
1. Activation and global configuration (admin)
Where: /admin/loyalty → Setup → General Settings
Required activation: loyalty_setting = 1
Without this, the after_payment_added hook is registered but returns immediately without any calculation.
2. Loyalty Rules — earn and redemption rules
Where: /admin/loyalty → Loyalty Rules → New Rule
A single rule defines both earning and redemption.
2a. Rule fields
| Field | Required | Description |
|---|---|---|
subject |
Yes | Rule name |
enable |
Yes | 1 = active |
start_date / end_date |
Yes | Validity period |
rule_base |
Yes | card_total / product_category / product |
client_group |
No | Target client group (0 = all) |
client |
No | Specific client IDs (CSV) |
minium_purchase |
No | Minimum order value to earn points |
max_amount_received |
No | Maximum points cap earned per transaction |
For card_total:
| Field | Description |
|---|---|
poin_awarded |
Points awarded per bracket |
purchase_value |
Value of one bracket (e.g.: 100 RON = 10 points) |
For product_category and product: add lines in Rule Details — each line specifies rel_type, rel_id (group or product ID) and loyalty_point per unit.
Special bonuses per rule:
| Field | Description |
|---|---|
create_account_point |
Bonus points when a new client account is created |
birthday_point |
Birthday bonus points (requires the custom field 'Birthday' on the client) |
2b. Redemption Tiers
Add at least one tier in the Redemption Details section of the rule:
| Field | Description |
|---|---|
point_from / point_to |
Point range for this tier |
point_weight |
Conversion rate: points → currency (e.g.: 0.05 = 1 pt = 0.05 RON) |
status |
'enable' = active tier |
redeem_portal |
1 = can be used in the client portal |
redeem_pos |
1 = can be used in OmniSales/POS |
min_poin_to_redeem |
Minimum points required to activate redemption |
redeemp_type |
'full' = full redemption · 'partial' = partial redemption |
3. Membership Rules — tiers
Where: /admin/loyalty → Membership → New Membership Rule
Defines tiers (e.g.: Bronze, Silver, Gold) based on accumulated points:
| Field | Description |
|---|---|
name |
Tier name (e.g.: "Gold") |
loyalty_point_from / loyalty_point_to |
Point range for this tier |
client_group |
Eligible client group (0 = all) |
card |
FK → loy_card (optional visual card template) |
4. Membership Programs — tier benefits
Where: /admin/loyalty → Membership Programs → New Program
A program links one or more tiers to a discount benefit:
| Field | Description |
|---|---|
program_name |
Program name |
membership |
Target tier IDs (CSV from loy_mbs_rule) |
discount |
Discount type: card_total / product_category / product |
discount_percent |
Discount percentage |
voucher_code |
Optional voucher code (for manual redemption) |
voucher_value |
Fixed voucher value |
minium_purchase |
Minimum order for discount activation |
start_date / end_date |
Program validity |
Note: When a membership program with a discount is applied to an invoice, the system automatically generates a credit note (
add_credit_mbs_program()) and applies it to the invoice. This credit note appears in the Credit Notes module.
5. The "Birthday" custom field (for birthday bonus)
Where: /admin/clients → client record → custom field Birthday (date_picker)
The field is created automatically by the module with slug customers_birthday. The birthday bonus is triggered by the cron (before_cron_run) once per year per client.
6. Points redemption
6a. On invoice (admin)
When creating/editing an invoice, the Redeem Points section appears if:
- The client has sufficient points (
≥ min_poin_to_redeem) - An active rule with a valid redemption tier exists
Staff selects the points to redeem → discount calculated automatically → written to loy_redeem_log.
6b. Client portal
The logged-in client sees the Redeem Points button on the invoice page (if redeem_portal=1 on the rule and the invoice is not already fully paid).
The client chooses the points → the system directly modifies invoices.total (reduces the total) and logs to loy_voucher_inv_log.
6c. OmniSales / POS
The redemption field appears in the OmniSales cart (if redeem_pos=1). Applied at order creation.
6d. Membership voucher
When entering a voucher code (apply_other_voucher filter), the system checks whether the client belongs to a tier with an active program that has that code. Returns voucher_value and minium_purchase.
7. Viewing client point balance
Where: /admin/clients/{id} → column loy_point on the client record
The current balance is stored directly in tblclients.loy_point (DECIMAL). The full transaction history is visible in Loyalty → Transactions filtered by client.
Required permissions
| Action | Permission |
|---|---|
| View Loyalty module | loyalty → view |
| Configure rules, cards, programs | is_admin() exclusively (require_loyalty_admin) |
| Client portal (redemption) | is_client_logged_in() |
Gotchas
| Problem | Cause | Solution |
|---|---|---|
| Points not added after payment | loyalty_setting ≠ 1 or rule expired |
Activate global toggle + check rule start_date/end_date |
| Client not matched by accumulation rule | client or client_group mismatch |
Verify the rule includes that client or their group |
| Birthday bonus not awarded | Birthday field not filled on client or cron inactive |
Fill in the field + check cron job |
| Redemption not appearing on invoice | min_poin_to_redeem not met or rule inactive |
Check point balance vs min_poin_to_redeem |
| Membership discount generates unexpected credit note | Correct behavior — add_credit_mbs_program() |
The credit note is normal; view it in Credit Notes |
| Points do not expire | No expiration mechanism exists | Correct behavior — points are permanent; limit via end_date on the rule |
Module references
- Clients — loy_point balance on the client record
- Invoices — redemption on invoice
- OmniSales — POS redemption
- Credit Notes — credit notes generated by membership programs