Розробка кешбек-системи на 1С-Бітрікс
Кешбек — це повернення частини вартості покупки на рахунок користувача у вигляді балів або грошей. У 1С-Бітрікс є вбудований механізм бонусних балів (модуль sale, підсистема discount), але він охоплює лише базові сценарії: нарахування фіксованого відсотка. Повноцінна кешбек-система — з історією нарахувань, правилами за категоріями, терміном згоряння та обмеженнями на оплату — потребує кастомної розробки поверх 1С-Бітрікс.
Архітектура зберігання даних
Кешбек — це окрема сутність, не тотожна «бонусним балам» 1С-Бітрікс. 1С-Бітрікс зберігає бонусні бали в 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;
}
// Створюємо знижку в 1С-Бітрікс
$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;
}
}
Згоряння невикористаного кешбеку
Агент 1С-Бітрікс щодня списує прострочений кешбек:
// Агент: \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 дні |
| Списання при оплаті | Інтеграція зі знижками 1С-Бітрікс | 2–3 дні |
| Згоряння та агент | Агент + логіка прострочення | 1 день |
| Особистий кабінет | Історія, баланс, інтерфейс оплати | 2–3 дні |







