Configuring Category-Based Cashback Accrual Rules in 1C-Bitrix
A marketer wants to give 5% cashback on electronics, 2% on household chemicals, and 0% on sale items. Rules must also be combinable: a "Gold" loyalty card adds +1% on top of the base rate in any category. The standard discount module (catalog.discount) is not suitable — it operates on price reductions, not account accruals. A separate rules system is required.
Rules Architecture
Rules are stored in the local_cashback_rules table. Minimum structure:
CREATE TABLE local_cashback_rules (
ID INT AUTO_INCREMENT PRIMARY KEY,
RULE_TYPE ENUM('category','product','user_group','promo') NOT NULL,
ENTITY_ID INT, -- infoblock section ID, product ID, or group ID
CASHBACK_PCT DECIMAL(5,2), -- accrual percentage
PRIORITY INT DEFAULT 10,-- lower value = higher priority
DATE_FROM DATE,
DATE_TO DATE,
ACTIVE CHAR(1) DEFAULT 'Y'
);
Rules with RULE_TYPE = 'category' are tied to catalog infoblock sections. When accruing cashback for an order, it is necessary to determine which section each product belongs to and find the applicable rule.
Determining a Product's Category
A product may belong to multiple sections (multiple assignment). For rule matching, the "main" section is used:
function getCashbackRateForProduct(int $productId): float
{
// Main section via b_iblock_element
$element = \CIBlockElement::GetByID($productId)->Fetch();
$sectionId = (int)$element['IBLOCK_SECTION_ID'];
// Look for a rule: first by exact section, then by parents
while ($sectionId > 0) {
$rule = CashbackRuleTable::getActiveRuleForSection($sectionId);
if ($rule) {
return (float)$rule['CASHBACK_PCT'];
}
// Traverse up the section tree
$section = \CIBlockSection::GetByID($sectionId)->Fetch();
$sectionId = (int)$section['IBLOCK_SECTION_ID'];
}
// Default rule
return CashbackConfig::getDefaultRate();
}
Rule inheritance through the section tree: if "Electronics" is set to 5% and "Laptops" has no explicit rule, laptops inherit 5% from the parent section. An explicit rule on a child section always overrides the parent.
Priority and Rule Combination
Complex logic with multiple simultaneously active rules — via priorities:
function resolveCashbackRate(int $productId, int $userId): float
{
$baseRate = getCashbackRateForProduct($productId);
// Additional rules by user group
$userGroups = CUser::GetUserGroup($userId);
$bonusRates = [];
foreach ($userGroups as $groupId) {
$rule = CashbackRuleTable::getQuery()
->setFilter([
'RULE_TYPE' => 'user_group',
'ENTITY_ID' => $groupId,
'ACTIVE' => 'Y',
'<=DATE_FROM' => new \Bitrix\Main\Type\Date(),
'>=DATE_TO' => new \Bitrix\Main\Type\Date(),
])
->setOrder(['PRIORITY' => 'ASC'])
->fetchObject();
if ($rule) {
$bonusRates[] = (float)$rule->getCashbackPct();
}
}
// Strategy: take the maximum group bonus + base category rate
$bonusRate = empty($bonusRates) ? 0 : max($bonusRates);
return $baseRate + $bonusRate;
}
The addition or replacement strategy is a business decision. For most loyalty programs: category rate + group bonus (addition), but not exceeding the maximum allowed percentage.
Exclusions: Promotional Products and Promo Periods
A zero rate on promotional products is implemented as a rule with CASHBACK_PCT = 0 and the highest priority (lowest numeric value in the PRIORITY field). A product is considered promotional if it has a discount applied through the catalog.discount mechanism or via the custom property IS_PROMO = Y.
Promo periods — the same rules with DATE_FROM and DATE_TO. Automatic activation/deactivation without developer intervention.
Accrual on Order Completion
Cashback is accrued when the order status changes to "Fulfilled" (not on payment — to avoid accruing on returned items):
AddEventHandler('sale', 'OnSaleStatusOrder', function(string $statusId, \Bitrix\Sale\Order $order) {
if ($statusId !== 'F') { // F = Fulfilled
return;
}
$userId = $order->getUserId();
$totalCashback = 0;
foreach ($order->getBasket() as $item) {
$productId = (int)$item->getProductId();
$rate = resolveCashbackRate($productId, $userId);
$cashback = $item->getPrice() * $item->getQuantity() * ($rate / 100);
$totalCashback += $cashback;
// Record per line item for detailed history
CashbackTransactionTable::add([
'USER_ID' => $userId,
'ORDER_ID' => $order->getId(),
'PRODUCT_ID' => $productId,
'AMOUNT' => $cashback,
'RATE' => $rate,
'TYPE' => 'accrual',
]);
}
CashbackBalanceTable::credit($userId, $totalCashback);
});
Managing Rules from the Admin Panel
The rules management interface is built with CAdminList + CAdminForm or a React component in the /local/admin/ section. Minimum feature set: rules list with filter by type/active status, edit form with the catalog section tree for category selection.
Timeline
| Task | Duration |
|---|---|
| Rules tables, basic category logic | 3–5 days |
| Group bonuses, exclusions, promo periods | 3–5 days |
| Accrual on order with detailed history | 2–3 days |
| Rules management interface in admin panel | 3–5 days |
| Full package | 2–3 weeks |







