Разработка кэшбэк-системы на 1С-Битрикс
Кэшбэк — возврат части стоимости покупки на счёт пользователя в виде баллов или денег. В Битрикс есть встроенный механизм бонусных баллов (модуль sale, подсистема discount), но он покрывает только базовые сценарии: начисление фиксированного процента. Полноценная кэшбэк-система — с историей начислений, правилами по категориям, сроком сгорания и ограничениями на оплату — требует кастомной разработки поверх Битрикс.
Архитектура хранения данных
Кэшбэк — это отдельная сущность, не тождественная «бонусным баллам» Битрикс. Битрикс хранит бонусные баллы в b_sale_user_account и b_sale_account_user_balance. Но для полноценного кэшбэка с историей, правилами начисления и сроком сгорания лучше создать собственную схему:
-- Кэшбэк-аккаунт пользователя
CREATE TABLE b_cashback_account (
ID INT AUTO_INCREMENT PRIMARY KEY,
USER_ID INT NOT NULL UNIQUE,
BALANCE DECIMAL(10,2) NOT NULL DEFAULT 0.00,
TOTAL_EARNED DECIMAL(10,2) NOT NULL DEFAULT 0.00, -- всего заработано
TOTAL_SPENT DECIMAL(10,2) NOT NULL DEFAULT 0.00, -- всего потрачено
UPDATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user (USER_ID)
);
-- История транзакций
CREATE TABLE b_cashback_transaction (
ID INT AUTO_INCREMENT PRIMARY KEY,
USER_ID INT NOT NULL,
ORDER_ID INT NULL,
TYPE ENUM('earn', 'spend', 'expire', 'adjust') NOT NULL,
AMOUNT DECIMAL(10,2) NOT NULL,
BALANCE_AFTER DECIMAL(10,2) NOT NULL,
DESCRIPTION VARCHAR(500) NOT NULL DEFAULT '',
STATUS ENUM('pending', 'confirmed', 'cancelled') NOT NULL DEFAULT 'pending',
EXPIRES_AT DATE NULL, -- дата сгорания для транзакций начисления
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_type (USER_ID, TYPE),
INDEX idx_order (ORDER_ID),
INDEX idx_expires (EXPIRES_AT, STATUS)
);
-- Правила начисления
CREATE TABLE b_cashback_rule (
ID INT AUTO_INCREMENT PRIMARY KEY,
NAME VARCHAR(255) NOT NULL,
CONDITION_TYPE ENUM('category', 'brand', 'product', 'order_total', 'all') NOT NULL,
CONDITION_VALUE VARCHAR(1000) NULL, -- JSON: {"iblock_section_ids": [1,2,3]}
CASHBACK_PERCENT DECIMAL(5,2) NOT NULL,
MIN_ORDER_AMOUNT DECIMAL(10,2) NOT NULL DEFAULT 0.00,
ACTIVE CHAR(1) NOT NULL DEFAULT 'Y',
SORT INT NOT NULL DEFAULT 100,
DATE_FROM DATE NULL,
DATE_TO DATE NULL,
INDEX idx_active_sort (ACTIVE, SORT)
);
Расчёт процента кэшбэка для товаров в корзине
Правила применяются по приоритету (SORT). Для каждой позиции корзины ищем наиболее подходящее правило:
// /local/lib/Cashback/RuleCalculator.php
namespace Local\Cashback;
class RuleCalculator
{
public static function calculateForOrder(\Bitrix\Sale\Order $order): array
{
$result = [];
$basket = $order->getBasket();
$activeRules = self::getActiveRules();
foreach ($basket as $basketItem) {
$productId = $basketItem->getProductId();
$price = $basketItem->getFinalPrice();
$qty = $basketItem->getQuantity();
// Получаем принадлежность товара к категориям и брендам
$productMeta = self::getProductMeta($productId);
$matchedRule = self::findRule($productMeta, $order->getPrice(), $activeRules);
if ($matchedRule) {
$cashbackAmount = round($price * $qty * $matchedRule['CASHBACK_PERCENT'] / 100, 2);
$result[] = [
'PRODUCT_ID' => $productId,
'PRODUCT_NAME' => $basketItem->getField('NAME'),
'RULE_ID' => $matchedRule['ID'],
'RULE_NAME' => $matchedRule['NAME'],
'PERCENT' => $matchedRule['CASHBACK_PERCENT'],
'CASHBACK_AMOUNT' => $cashbackAmount,
];
}
}
return $result;
}
private static function findRule(array $productMeta, float $orderTotal, array $rules): ?array
{
foreach ($rules as $rule) {
if ($rule['MIN_ORDER_AMOUNT'] > 0 && $orderTotal < $rule['MIN_ORDER_AMOUNT']) {
continue;
}
switch ($rule['CONDITION_TYPE']) {
case 'all':
return $rule;
case 'category':
$catIds = json_decode($rule['CONDITION_VALUE'], true)['iblock_section_ids'] ?? [];
if (array_intersect($productMeta['SECTION_IDS'], $catIds)) {
return $rule;
}
break;
case 'brand':
$brands = json_decode($rule['CONDITION_VALUE'], true)['brands'] ?? [];
if (in_array($productMeta['BRAND'], $brands)) {
return $rule;
}
break;
case 'product':
$productIds = json_decode($rule['CONDITION_VALUE'], true)['product_ids'] ?? [];
if (in_array($productMeta['ID'], $productIds)) {
return $rule;
}
break;
}
}
return null;
}
private static function getProductMeta(int $productId): array
{
$el = \CIBlockElement::GetByID($productId)->GetNextElement();
if (!$el) {
return ['ID' => $productId, 'SECTION_IDS' => [], 'BRAND' => ''];
}
$fields = $el->GetFields();
$props = $el->GetProperties();
// Собираем все разделы (включая родительские)
$sectionIds = [];
if ($fields['IBLOCK_SECTION_ID']) {
$sectionIds = \Local\Catalog\SectionHelper::getParentIds(
(int)$fields['IBLOCK_SECTION_ID']
);
}
return [
'ID' => $productId,
'SECTION_IDS' => $sectionIds,
'BRAND' => $props['BRAND']['VALUE'] ?? '',
];
}
}
Начисление кэшбэка: pending → confirmed
Кэшбэк начисляется в статусе pending сразу после оформления заказа, подтверждается после выполнения заказа (статус F). Это защита от возвратов:
// Начисление при создании заказа (pending)
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale', 'OnSaleOrderSaved',
function (\Bitrix\Main\Event $event) {
$order = $event->getParameter('ENTITY');
if (!$order->isNew()) {
return;
}
$calculations = \Local\Cashback\RuleCalculator::calculateForOrder($order);
$totalCashback = array_sum(array_column($calculations, 'CASHBACK_AMOUNT'));
if ($totalCashback <= 0) {
return;
}
$holdDays = (int)\Bitrix\Main\Config\Option::get('local.cashback', 'hold_days', 14);
\Local\Cashback\AccountManager::createTransaction(
$order->getUserId(),
'earn',
$totalCashback,
"Кэшбэк за заказ #{$order->getId()} (ожидание подтверждения)",
$order->getId(),
'pending',
date('Y-m-d', strtotime("+{$holdDays} days"))
);
}
);
// Подтверждение при выполнении заказа
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale', 'OnSaleOrderStatusChange',
function (\Bitrix\Main\Event $event) {
$order = $event->getParameter('ENTITY');
if ($order->getField('STATUS_ID') !== 'F') {
return;
}
\Local\Cashback\AccountManager::confirmOrderTransactions($order->getId());
}
);
Списание кэшбэка при оплате
Кэшбэком можно оплатить часть следующего заказа. Ограничение — не более 50% стоимости заказа (или другой процент из настроек):
// /local/lib/Cashback/PaymentProcessor.php
class PaymentProcessor
{
public static function applyToOrder(
\Bitrix\Sale\Order $order,
float $cashbackToSpend
): \Bitrix\Main\Result {
$result = new \Bitrix\Main\Result();
$userId = $order->getUserId();
$maxSpend = $order->getPrice() * 0.5; // максимум 50%
$available = AccountManager::getBalance($userId);
$toSpend = min($cashbackToSpend, $available, $maxSpend);
if ($toSpend <= 0) {
$result->addError(new \Bitrix\Main\Error('Недостаточно кэшбэка'));
return $result;
}
// Создаём скидку в Битрикс
$discount = \Bitrix\Sale\OrderDiscount::create($order);
$discount->setFields([
'DISCOUNT_VALUE' => $toSpend,
'DISCOUNT_TYPE' => 'F', // Fixed amount
'DISCOUNT_NAME' => 'Оплата кэшбэком',
]);
// Списываем транзакцию
AccountManager::createTransaction(
$userId,
'spend',
$toSpend,
"Списание кэшбэка в счёт заказа #{$order->getId()}",
$order->getId(),
'confirmed'
);
$result->setData(['applied' => $toSpend]);
return $result;
}
}
Сгорание неиспользованного кэшбэка
Агент Битрикс раз в сутки списывает просроченный кэшбэк:
// Агент: \Local\Cashback\ExpirationAgent::run()
$connection = \Bitrix\Main\Application::getConnection();
// Находим просроченные pending/confirmed транзакции
$expired = $connection->query("
SELECT USER_ID, SUM(AMOUNT) as TOTAL_AMOUNT
FROM b_cashback_transaction
WHERE TYPE = 'earn'
AND STATUS = 'confirmed'
AND EXPIRES_AT IS NOT NULL
AND EXPIRES_AT < CURDATE()
GROUP BY USER_ID
")->fetchAll();
foreach ($expired as $row) {
AccountManager::createTransaction(
$row['USER_ID'],
'expire',
$row['TOTAL_AMOUNT'],
'Сгорание кэшбэка по истечении срока',
null,
'confirmed'
);
// Помечаем истёкшие транзакции как обработанные
$connection->queryExecute("
UPDATE b_cashback_transaction
SET STATUS = 'cancelled'
WHERE USER_ID = ? AND TYPE = 'earn' AND STATUS = 'confirmed'
AND EXPIRES_AT < CURDATE()
", [$row['USER_ID']]);
}
Личный кабинет: история и баланс
В личном кабинете покупателя — блок кэшбэка:
// Данные для шаблона
$balance = \Local\Cashback\AccountManager::getBalance($USER->GetID());
$history = \Local\Cashback\AccountManager::getHistory($USER->GetID(), 10);
$pending = \Local\Cashback\AccountManager::getPendingAmount($USER->GetID());
Выводим: текущий баланс, сумма в ожидании (из незавершённых заказов), история транзакций с датой и описанием.
Сроки разработки
| Этап | Содержание | Срок |
|---|---|---|
| Схема данных | Таблицы, индексы, Account Manager | 1–2 дня |
| Правила начисления | CRUD-интерфейс в админке + калькулятор | 2–3 дня |
| Начисление и подтверждение | Обработчики событий заказа | 1–2 дня |
| Списание при оплате | Интеграция со скидками Битрикс | 2–3 дня |
| Сгорание и агент | Агент + логика просрочки | 1 день |
| Личный кабинет | История, баланс, интерфейс оплаты | 2–3 дня |







