Разработка кэшбэк-системы на 1С-Битрикс

Наша компания занимается разработкой, поддержкой и обслуживанием решений на Битрикс и Битрикс24 любой сложности. От простых одностраничных сайтов до сложных интернет магазинов, CRM систем с интеграцией 1С и телефонии. Опыт разработчиков подтвержден сертификатами от вендора.
Предлагаемые услуги
Показано 1 из 1 услугВсе 1626 услуг
Разработка кэшбэк-системы на 1С-Битрикс
Средняя
~1-2 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Разработка на базе Битрикс, Битрикс24, 1С для компании Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Разработка на базе 1С Предприятие для компании МИРСАНБЕЛ
    747
  • image_crm_dolbimby_434_0.webp
    Разработка сайта на CRM Битрикс24 для компании DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Разработка на базе Битрикс24 для компании ТЕХНОТОРГКОМПЛЕКС
    976

Разработка кэшбэк-системы на 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 дня