abderrahimghazali / sylius-loyalty-plugin
Points-based loyalty and rewards system for Sylius 2.x
Package info
github.com/abderrahimghazali/sylius-loyalty-plugin
Type:sylius-plugin
pkg:composer/abderrahimghazali/sylius-loyalty-plugin
Requires
- php: ^8.2
- doctrine/orm: ^2.17 || ^3.0
- sylius/sylius: ^2.1
- symfony/framework-bundle: ^7.0
- symfony/stimulus-bundle: ^2.0
- symfony/ux-live-component: ^2.0
- symfony/ux-twig-component: ^2.0
Requires (Dev)
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2026-03-27 21:54:08 UTC
README
Sylius Loyalty Plugin
A points-based loyalty and rewards system for Sylius 2.x e-commerce stores.
Overview
SyliusLoyaltyPlugin adds a complete loyalty program to any Sylius 2.x store. Customers earn points on purchases, redeem them as discounts on the cart page, unlock tier-based multipliers, and receive bonus points for registration, birthdays, and first orders — all configurable per channel from the admin panel.
Key Features
- Multi-channel — Each channel has independent earning rates, redemption rates, expiry, and bonus settings
- Loyalty rules — Override earning rates for specific products with per-rule points configuration and multi-channel support
- Points earning — Configurable default points per currency unit on every order
- Cart redemption — Spend points as a monetary discount on the cart page
- Points expiry — Automatic expiration with cron command + 30-day warnings
- Bonus events — Registration, birthday, and first-order bonuses (toggle on/off per channel)
- Tier system — Bronze / Silver / Gold with earning multipliers (tiers only go up)
- Admin panel — Full management: accounts, transactions, manual adjustments, per-channel config
- Customer account — Points balance, tier badge, paginated transaction history with running balance
- REST API — Headless-ready endpoints for balance and redemption
- Workflow integration — Points deducted on order complete, restored on cancel/refund
- Translations — English, French, German, Spanish, Polish, Portuguese
Requirements
| Dependency | Version |
|---|---|
| PHP | ^8.2 |
| Sylius | ~2.1 |
| Symfony | ^7.0 |
Installation
composer require abderrahimghazali/sylius-loyalty-plugin
1. Register the plugin
// config/bundles.php return [ // ... Abderrahim\SyliusLoyaltyPlugin\SyliusLoyaltyPlugin::class => ['all' => true], ];
2. Import routes
# config/routes/sylius_loyalty.yaml sylius_loyalty: resource: '@SyliusLoyaltyPlugin/config/routes.yaml'
3. Extend your Order entity
Add the loyalty trait to your Order entity so customers can redeem points on the cart page:
// src/Entity/Order/Order.php namespace App\Entity\Order; use Abderrahim\SyliusLoyaltyPlugin\Entity\Order\LoyaltyOrderInterface; use Abderrahim\SyliusLoyaltyPlugin\Entity\Order\LoyaltyOrderTrait; use Sylius\Component\Core\Model\Order as BaseOrder; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] #[ORM\Table(name: 'sylius_order')] class Order extends BaseOrder implements LoyaltyOrderInterface { use LoyaltyOrderTrait; // ...existing code... }
4. Register the Stimulus controller (for the cart widget)
Add the plugin JS dependency to your package.json:
{
"dependencies": {
"@abderrahimghazali/sylius-loyalty-plugin": "file:vendor/abderrahimghazali/sylius-loyalty-plugin/assets"
}
}
Register the controller in assets/shop/controllers.json:
{
"controllers": {
"@abderrahimghazali/sylius-loyalty-plugin": {
"loyalty-redemption": {
"enabled": true,
"fetch": "eager"
}
}
}
}
Then rebuild assets:
npm install npm run build
5. Run migrations
php bin/console doctrine:migrations:diff php bin/console doctrine:migrations:migrate
6. Seed the default configuration
php bin/console loyalty:install
This creates a default loyalty configuration for each channel. You can then customize settings per channel from the admin panel under Configuration > Loyalty configuration.
7. Set up cron jobs
# Expire old points (run daily) php bin/console loyalty:expire-points # Award birthday bonuses (run daily) php bin/console loyalty:birthday-bonus
Architecture
Domain Model
Channel ──1:1──▶ LoyaltyConfiguration
(earning rate, redemption rate, expiry, bonuses)
Channel ──N:M──▶ LoyaltyRule ──N:M──▶ Product
(name, enabled, points rate per product)
Customer ──1:1──▶ LoyaltyAccount ──1:N──▶ PointTransaction
│ (earn/redeem/expire/adjust/bonus)
│
└───N:1──▶ LoyaltyTier
(Bronze/Silver/Gold)
Points are shared across channels (one account per customer), while earning/redemption rates are configured per channel.
Entities
| Entity | Purpose |
|---|---|
LoyaltyAccount |
Per-customer account with balance, lifetime points, tier |
PointTransaction |
Ledger entry — signed points, type, optional order link, expiry |
LoyaltyTier |
Tier with min-points threshold, earning multiplier, color |
LoyaltyConfiguration |
Per-channel config: earning rate, redemption rate, expiry, bonuses |
LoyaltyRule |
Per-product earning rate override, multi-channel |
Sylius Integration Points
| Extension Point | What It Does |
|---|---|
OrderProcessorInterface (priority 5) |
Applies loyalty discount adjustment after taxes |
sylius.order.post_complete event |
Awards earn points + first-order bonus on order completion |
sylius.order.post_cancel event |
Revokes earned points |
sylius.customer.post_register event |
Awards registration bonus |
workflow.sylius_order_checkout.completed.complete |
Deducts redeemed points from balance |
workflow.sylius_order.completed.cancel |
Restores redeemed points |
workflow.sylius_payment.completed.refund |
Restores redeemed points on refund |
sylius.menu.admin.main event |
Adds menu items under Customers, Marketing & Configuration |
| Twig hooks | Cart widget, cart/checkout summary, customer show section, account menu |
Multi-Channel Support
Each Sylius channel can have its own loyalty configuration:
| Setting | US Store | EU Store | B2B Store |
|---|---|---|---|
| Earning rate | 1 pt / $1 | 2 pts / €1 | Disabled |
| Redemption rate | 100 pts = $1 | 50 pts = €1 | N/A |
| Registration bonus | 100 pts | 200 pts | 0 |
| Birthday bonus | 200 pts | 500 pts | 0 |
| Tiers enabled | Yes | Yes | No |
Points are shared across channels — a customer earns on one store and redeems on another. Rates are applied based on the order's channel.
Manage per-channel settings at Configuration > Loyalty configuration in the admin panel.
Shop Features
Cart Redemption Widget
On the cart page, logged-in customers can redeem points as a discount. The widget uses Symfony UX Live Components for seamless interaction without page reloads.
- Input field with placeholder showing available balance
- "Apply points" button (triggers Live Component re-render)
- Applied state shows a badge with points count, discount value, and a remove button
- Loyalty discount line appears in the cart summary and throughout the checkout sidebar
- Automatic clamping: can't exceed balance or order total
Customer Account — Loyalty Page
Accessible from the account sidebar menu:
- Current balance + redeemable monetary value
- Tier badge with multiplier info
- Expiry warning for points expiring within 30 days
- Paginated transaction history table with running balance column
Admin Features
Loyalty Accounts
Grid view of all customer loyalty accounts with balance, lifetime points, tier, and status. Click through to a detail page showing paginated transaction history.
Manual Point Adjustment
From any loyalty account detail page, admins can add or deduct points with a required reason field. Positive values create an Adjust (credit) transaction, negative values create a Deduct (debit) transaction.
Tier Management
Full CRUD for loyalty tiers under Marketing > Loyalty tiers. Code and position are auto-generated from the tier name.
| Field | Description |
|---|---|
| Name | Display name (e.g., "Bronze") — code is auto-generated |
| Min Points | Lifetime points threshold to reach this tier |
| Multiplier | Earning multiplier (e.g., 1.5x for Silver) |
| Color | Badge color (hex, rendered in admin and shop) |
Per-Channel Configuration
Under Configuration > Loyalty configuration, admins see a table of all channels and can configure each independently:
- Points per currency unit
- Redemption rate (points per 1 currency unit)
- Expiry period in days
- Enable/disable tier system
- Toggle and configure bonus events (registration, birthday, first order)
Settings are stored in the database and take effect immediately without redeployment.
Loyalty Rules
Under Marketing > Loyalty rules, admins can override the default earning rate for specific products. Each rule has:
- Name — descriptive label for the rule
- Products — select one or more products via autocomplete
- Points per currency unit — custom earning rate (set to 0 to exclude products from earning)
- Channels — select which channels the rule applies to
- Enabled — toggle the rule on/off
When a product in an order matches a loyalty rule for that channel, the rule's rate is used instead of the channel's default rate.
Example rules:
| Rule | Products | Rate | Channels |
|---|---|---|---|
| Double on gift sets | Gift Set A, Gift Set B | 2 pts/€1 | All |
| No points on gift cards | Gift Card | 0 pts/€1 | All |
| Premium bonus | Limited Edition Watch | 10 pts/€1 | Fashion Web Store |
API Endpoints (Headless)
For headless/SPA storefronts, the plugin provides REST endpoints. All shop endpoints verify the authenticated user owns the order.
Shop API
POST /api/v2/shop/orders/{tokenValue}/loyalty-redemption
Body: { "pointsToRedeem": 500 }
→ { "pointsRedeemed": 500, "discountAmount": 500, "orderTotal": 9500 }
DELETE /api/v2/shop/orders/{tokenValue}/loyalty-redemption
→ { "pointsRedeemed": 0, "discountAmount": 0, "orderTotal": 10000 }
GET /api/v2/shop/loyalty/account
→ Balance, lifetime points, tier info
Admin API
GET /api/v2/admin/loyalty/accounts
GET /api/v2/admin/loyalty/accounts/{id}
PATCH /api/v2/admin/loyalty/accounts/{id}
Edge Cases Handled
- Pessimistic DB locking prevents double-spend on concurrent checkouts, cancellations, and refunds
- API endpoints require
ROLE_USERand verify the authenticated user owns the order - Balance cannot go negative (clamped in service layer + guard in entity)
- Points redemption cannot exceed available balance (clamped, validated in cart form and API)
- Discount cannot exceed order total (capped, points recalculated)
- Guest checkouts cannot use loyalty points (guarded)
- Disabled accounts are excluded from earning and redemption
- Duplicate point awards are prevented (idempotent per order)
- Duplicate revocations are prevented (idempotent per order)
- First-order bonus awarded only once (idempotent)
- Points are reserved at cart, deducted only on order completion
- Cancelled/refunded orders restore redeemed points (idempotent, locked)
- Birthday bonus awarded at most once per calendar year per channel
- Tiers only upgrade, never demote (based on lifetime points)
- CSRF protection on all admin forms
- Admin point adjustment descriptions are sanitized
- Expire points command uses batched processing for large datasets
Translations
The plugin ships with translations for:
| Language | File |
|---|---|
| English | messages.en.yaml |
| French | messages.fr.yaml |
| German | messages.de.yaml |
| Spanish | messages.es.yaml |
| Polish | messages.pl.yaml |
| Portuguese | messages.pt.yaml |
Running Tests
composer install vendor/bin/phpunit
71 unit tests covering entities, services, order processing, and all event listeners.
Screenshots
Cart — Redeem points
Customer Account — Loyalty points & transaction history
Admin — Loyalty tiers management
License
This plugin is released under the MIT License.


