How to Implement a Custom Reward Points System in Magento 2

In this post, we’ll walk through how to implement a custom reward points system in Magento 2. The goal is to give you a practical, actionable blueprint that a teammate could follow over a long afternoon or an uninterrupted weekend. You’ll learn not only the “how” but also the “why” behind a robust points program that scales with your store and customer base.
Why build a custom rewards system instead of using a ready-made extension?
Magento 2 ships with a lot of powerful features, but every store is unique. A one-size-fits-all rewards extension may cover common scenarios but often falls short for nuanced pricing rules, custom customer segments, or bespoke UX needs. Building a custom reward points system gives you:
- Full control over data models, performance, and security.
- Flexibility to match your business rules (earn/redemption, expiration, stacking, etc.).
- Seamless data integration with existing price rules, customer groups, and promotions.
- Tailored admin dashboards and reporting that match your internal processes.
Yes, it’s more work upfront. But with a clear plan and well-scoped milestones, you’ll end up with a reliable, maintainable system that pays off as you grow.
High-level architecture: what you’ll build
Let’s sketch the main pieces. A custom rewards module in Magento 2 typically includes:
- Entities and persistence for accounts, transactions, and rules.
- Service layer to encapsulate business logic for earning and redeeming points.
- Event observers and plugins to hook into order placement, invoices, reviews, signups, and admin actions.
- Admin UI components (grid, forms) to configure rules and view user-level participation.
The data model should be lean but expressive. A simple yet scalable approach is to model points as indivisible units with timestamps and a type that categorizes the origin (order_purchase, review, signup, promo, etc.). Points have a balance and can be redeemed in units or monetary value depending on your rules.
Data model overview: tables you’ll need
The following tables form a pragmatic starting point. Adapt names to your namespace, e.g. magefine_reward or vendor_reward.
- reward_account — per-customer balance and summary.
- reward_transaction — a ledger of all earning and redemption events.
- reward_rule — definitions for earning/ redemption rules and their applicability (customer groups, product categories, cart subtotals).
- reward_rule_usage — optional table to track how often a rule can be used within a window (e.g., 1x per order).
Here's a compact sketch of the schema you might start with. Keep in mind Magento’s EAV and flat table patterns, and adapt to your deployment style. This is not a full schema dump but a practical starting point.
-- reward_account: simple balance per customer
CREATE TABLE reward_account (
account_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
balance INT NOT NULL DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- reward_transaction: ledger of earned/redeemed points
CREATE TABLE reward_transaction (
txn_id BIGINT PRIMARY KEY,
account_id BIGINT NOT NULL,
points INT NOT NULL,
type ENUM('earn','redeem') NOT NULL,
description VARCHAR(255),
reference_id BIGINT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- reward_rule: how points are earned or redeemed
CREATE TABLE reward_rule (
rule_id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type ENUM('earn','redeem') NOT NULL,
points_per_unit DECIMAL(10,2) NOT NULL,
active BOOLEAN DEFAULT TRUE,
customer_group_id BIGINT,
min_order_subtotal DECIMAL(12,2) DEFAULT 0,
start_date DATE,
end_date DATE
);
Module bootstrap in Magento 2: a minimal skeleton
We’ll boot a small, clean module to house the rewards logic. The module will be named Vendor_CustomRewards. Here are the essential files and their roles. You can copy, adapt, and expand as you go.
1) registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Vendor_CustomRewards',
__DIR__
);
2) etc/module.xml
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Vendor_CustomRewards" setup_version="1.0.0"/>
</config>
3) A basic setup script to create tables can be added under Setup/InstallSchema.php or better, using declarative schema in Magento 2.3+. Example snippet to create reward_account table is below.
<?php
namespace Vendor\CustomRewards\Setup;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\DB\Ddl\Table;
class InstallSchema implements InstallSchemaInterface
{
public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$installer = $setup;
$installer->startSetup();
$table = $installer->getConnection()->newTable(
$installer->getTable('reward_account')
)
->addColumn('account_id', Table::TYPE_BIGINT, null, ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], 'Account ID')
->addColumn('customer_id', Table::TYPE_BIGINT, null, ['nullable' => false], 'Customer ID')
->addColumn('balance', Table::TYPE_INTEGER, null, ['nullable' => false, 'default' => 0], 'Balance')
->addColumn('updated_at', Table::TYPE_TIMESTAMP, null, ['nullable' => false, 'default' => Table::TIMESTAMP_INIT_UPDATE], 'Updated At');
$installer->getConnection()->createTable($table);
$installer->endSetup();
}
}
In practice, you’ll typically rely on declarative schema (db_schema.xml) for Magento 2.3+ but this gives you a feel for the approach.
Earning points: the core business logic
Let’s implement a simple, robust earning flow. We’ll capture three popular triggers: (1) order completion, (2) product reviews, and (3) new customer signups. We’ll also expose a service class to centralize the math and state changes so it’s easy to test and maintain.
Architecture tip: keep earning rules data-driven. Use a Rule object or a small data transfer object (DTO) that encapsulates the origin, points, and applicability (customer group, subtotal thresholds). Then have a single calculator that converts an event into a points delta and a transaction description.
Example: earning on order completion. We assume a simple rule: 1 point for every $1 spent above a threshold, with a cap per order per customer. This keeps the math straightforward while still giving meaningful rewards.
// src/Model/EarningEngine.php
namespace Vendor\CustomRewards\Model;
class EarningEngine
{
protected $pointsPerDollar;
protected $minSubtotal;
public function __construct($pointsPerDollar = 1, $minSubtotal = 20)
{
$this->pointsPerDollar = $pointsPerDollar;
$this->minSubtotal = $minSubtotal;
}
public function calculate($orderSubtotal, $customerGroupId)
{
if ($orderSubtotal < $this->minSubtotal) {
return 0;
}
// Simple rule: earn based on subtotal and possibly group multiplier
$multiplier = 1.0;
if ($customerGroupId == 1) { // e.g. Wholesale
$multiplier = 1.25;
}
$points = (int) floor(($orderSubtotal) * $this->pointsPerDollar * $multiplier);
return max(0, $points);
}
}
Usage on order completion (simplified):
// Observer: sales_order_place_after or sales_order_invoice_pay
namespace Vendor\CustomRewards\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Sales\Model\Order;
class EarnOnOrderComplete implements ObserverInterface
{
public function execute(
\Magento\Framework\Event\Observer $observer
) {
/** @var Order $order */
$order = $observer->getEvent()->getOrder();
$customerId = $order->getCustomerId();
$subtotal = (float) $order->getSubtotal();
// Resolve customer group from customer model if needed
$groupId = 0; // fetch from customer if you have the API
$engine = new \Vendor\CustomRewards\Model\EarningEngine(1, 20);
$points = $engine->calculate($subtotal, $groupId);
// Persist: create reward_transaction and update reward_account balance
// ... (omitted for brevity: call a RewardService)
}
}
Note: In a production module you’d inject dependencies via constructor, avoid new, and handle edge cases like refunds and partial captures. This snippet demonstrates the core idea: encapsulate the calculation and push a transaction into the ledger.
Redeeming points: applying rewards at checkout
Redemption is arguably trickier than earning because it directly affects price and margin. A practical approach is to let customers apply a fixed-value deduction or a percentage-based discount using a custom cart rule or a dedicated checkout hook. Here is a simple, safe pattern:
- Expose a UI control on the cart/checkout where users can specify how many points to redeem (bounded by their balance).
- Translate points into a monetary discount using a conversion rate (e.g., 100 points = $1).
- Apply the discount in the totals calculation without bypassing Magento’s standard discount flows, so taxes/shipping remain coherent.
Code sketch (controller or plugin that adjusts quote totals):
// Simplified example: apply points as a discount in quote totals collector
namespace Vendor\CustomRewards\Model\Totals;
class PointsDiscount extends \Magento\Quote\Model\Quote\Address\Total\Subtotal
{
protected function collectTotals() {
// Load points balance for current customer
$points = 500; // example; fetch from account
$rate = 0.01; // 100 points = $1
$discount = (int) ($points * $rate);
$this->setTotalAmount(-$discount);
$this->setBaseTotalAmount(-$discount);
$this->getAddress()->setDiscountDescription('Reward points discount');
return $this;
}
}
Important: this is a simplified illustration. In a real module you’d implement a proper price resolver, ensure discount tax and shipping calculations are correct, and store redemption history in reward_transaction. Also consider preventing double-spends when orders are canceled or refunded.
Integrating with price rules and customer groups
Magento already has customer groups and pricing rules; you can reuse and extend them to govern rewards. A few practical strategies:
- Use customer_group_id to tailor earning multipliers and redemption limits per group.
- Incorporate catalog price rules such as category-based earning multipliers or product-specific exceptions.
- Allow tiered earning: new customers earn 10% of base rate, gold tier 25%, etc., with clear progress bars in the account area to encourage engagement.
Implementation notes:
- Attach rules to events like customer_login or customer_register to grant welcome bonuses safely and trackable.
- Use a central RuleEngine service that converts a rule into a concrete transaction (points delta, description, effective date window) and persists it to reward_transaction.
- Always guard against edge cases: refunds, partial captures, chargebacks, and manual adjustments.
Concrete example: a rule that grants 2x points for shipping subtotal above a threshold for premium groups. You’d implement this as a trigger in the EarnOnOrderComplete observer and factor in customer group and a per-order cap.
Acquisition and redemption methods: step-by-step approaches
To keep customers engaged, you want diverse, clear channels for earning and redeeming points. Below are practical methods and how to implement them in Magento 2.
1) Purchases
Base earning on order subtotal, product categories, and customer group. A typical approach is tiered earning with caps to prevent abuse and to keep promotions sustainable.
// in your observer from order complete
$pointsEarned = $engine->calculate($orderSubtotal, $groupId);
$transaction = [
'account_id' => $accountId,
'points' => $pointsEarned,
'type' => 'earn',
'description' => 'Earned from order #' . $orderId,
'reference_id' => $orderId,
];
// persist to reward_transaction and update reward_account balance
2) Reviews
Reward customers who leave reviews with a fixed amount or a multiplier for trusted reviewers. Listen to events such as review_save_after and create a transaction, with a limit to avoid spam.
namespace Vendor\CustomRewards\Observer;
class EarnOnReview implements \Magento\Framework\Event\ObserverInterface
{
public function execute(\Magento\Framework\Event\Observer $observer) {
// Pseudo-code: check review quality, avoid fake reviews
$points = 20; // or based on rating
// Create transaction
}
}
3) Signups
Welcome bonuses drive early engagement. Upon account creation, grant a modest number of points with an expiration and clear messaging in the account area.
// Event: customer_register_success
$points = 50;
// create a transaction for the new account's reward balance
4) Other channels
Consider linking with newsletter signups, first purchase, birthday bonuses, and seasonal campaigns. The important thing is to keep a clear, consistent policy that customers can understand.
Admin dashboards: tracking the loyalty program
A good admin UI helps your team monitor, adjust, and optimize the rewards program. At minimum you’ll want:
- Customer-level dashboards with balance, recent transactions, and activity trends.
- Global metrics: total points earned, redeemed, and outstanding balances by month.
- Rule management: enable/disable rules, set multipliers, and define guard rails.
- Audit trails: who changed what rule and when.
Code sketch: a simple grid in adminhtml to show customer balances. You’d build a UI component (listing) using Magento's UI components framework or a simple grid block.
// Admin grid class skeleton (very high level)
class BalanceGrid extends \Magento\Backend\Block\Widget\Grid\Extended {
protected function _prepareCollection() {
// $collection = $this->_customerCollectionFactory->create()
// join with reward_account and reward_transaction
}
protected function _prepareColumns() {
// Define columns: customer_email, balance, updated_at, total_earned, total_redeemed
}
}
Best practices to maximize engagement without overloading the system
- Start with a minimal viable program: clear messaging, predictable earning, and a simple redemption flow.
- Use data-driven rules: tie earnings to customer groups and product categories to encourage high-value purchases.
- Limit redemption to prevent margin erosion and avoid stacking conflicts with other promotions.
- Monitor performance: KPIs like PTO (points turnover rate), average order value, and repeat purchase rate.
- Transparent expiration rules: nothing kills trust like “points vanish at random.”
Performance considerations:
- Leverage Magento’s cache and event-driven architecture to minimize overhead. Perform heavy calculations in dedicated services, not in observers attached to hot paths.
- Use a background queue for batch processing of large point grants or expiring points to avoid blocking checkout flows.
- Persist transactions in an append-only ledger (reward_transaction) to simplify audits and debugging.
UX tips to maximize engagement:
- Show a clear balance progress bar and a “how many points to next tier” hint on the account page.
- Provide a clear, intuitive redemption UI with real-time discount estimation in the cart.
- Communicate the value of points in real-world terms (e.g., “500 points = $5”).
Step-by-step: getting started with a minimal viable module
Here’s a pragmatic, incremental approach you can follow with your team. It emphasizes small, testable changes and continuous improvement.
Step 1 — Define scope and data model
Agree on the key entities, the data flows, and the user journeys. Define a minimal dataset to begin with: accounts, transactions, and a couple of rules. Create a lightweight data schema and migrate it to your Magento install as declarative schema or install scripts.
Step 2 — Scaffold the module
Create a Magento 2 module skeleton (registration.php, etc/module.xml). This gives you a stable base for development and testing.
// registration.php and module.xml shown earlier
Step 3 — Implement a simple earning rule
Begin with a straightforward rule: earn 1 point per dollar above a threshold, per order. Build a small service to compute the points and persist a reward_transaction and update reward_account balance.
// Minimal service sketch
namespace Vendor\CustomRewards\Service;
class EarnService {
protected $pointsPerDollar = 1;
public function calculate($subtotal) { return max(0, (int) floor($subtotal * $this->pointsPerDollar)); }
public function credit($customerId, $points) {
// Get or create account, then increment balance and insert transaction
}
}
Step 4 — Hook into order completion
Register an observer for the order payment or invoice created event, compute points using the earning service, and persist. Keep this isolated and testable.
// Observer skeleton provided earlier
Step 5 — Implement redemption flow in cart
Expose a field on the cart where users can enter how many points to redeem. Convert points to money using a fixed rate, and apply a discount that respects taxes and shipping. Validate the input on the server side.
Step 6 — Admin: basic dashboards
Provide simple reporting components to show balances by customer and totals by month. Start small and iterate based on feedback from your growth team.
Step 7 — Testing and governance
Write unit tests for the earning and redemption logic. Implement integration tests that simulate order flows with different customer groups and product categories. Maintain a changelog and a backup plan for rules that cause unexpected behavior.
Common pitfalls and how to avoid them
- Overcomplicating the rule engine. Start simple, then add rules as you validate user behavior.
- Forgetting refunds. Always plan for refunds and partial refunds to reverse point transactions cleanly.
- Unclear expiration policies. Communicate clearly and consider a semantic UI that shows expiration timelines.
- Performance overhead. Use asynchronous processing for heavy point grants and periodic reconciliation.
Conclusion
Building a custom reward points system in Magento 2 is a meaningful investment that pays off through higher engagement, better data, and greater control over customer incentives. By starting with a lean data model, a minimal viable module, and a clear set of rules, you can iterate quickly while maintaining reliability for your customers. If you want a tested, battle-ready solution with ongoing updates and professional support, MageFine offers Magento 2 extensions and hosting that can help you scale your loyalty program without reinventing the wheel. Think of this post as a blueprint rather than a final product—the specifics will depend on your business rules, margins, and growth goals.