abderrahimghazali/sylius-loyalty-plugin

Points-based loyalty and rewards system for Sylius 2.x

Maintainers

Package info

github.com/abderrahimghazali/sylius-loyalty-plugin

Type:sylius-plugin

pkg:composer/abderrahimghazali/sylius-loyalty-plugin

Statistics

Installs: 41

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v2.0.0 2026-03-27 21:53 UTC

This package is auto-updated.

Last update: 2026-03-27 21:54:08 UTC


README

Sylius Logo

Sylius Loyalty Plugin

A points-based loyalty and rewards system for Sylius 2.x e-commerce stores.

CI Latest Version PHP Version License Sylius 2.x Symfony 7.x PHPStan Level 5

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_USER and 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

Cart redemption widget

Customer Account — Loyalty points & transaction history

Customer loyalty account

Admin — Loyalty tiers management

Admin loyalty tiers

License

This plugin is released under the MIT License.