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

Наша компанія займається розробкою, підтримкою та обслуговуванням рішень на Бітрікс та Бітрікс24 будь-якої складності. Від простих односторінкових сайтів до складних інтернет-магазинів, CRM систем з інтеграцією 1С та телефонії. Досвід розробників підтверджено сертифікатами від вендора.
Пропоновані послуги
Показано 1 з 1 послугУсі 1626 послуг
Розробка кешбек-системи на 1С-Бітрікс
Середня
~1-2 тижні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851
  • 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
    585
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Розробка на базі 1С Підприємство для компанії МИРСАНБЕЛ
    751
  • image_crm_dolbimby_434_0.webp
    Розробка сайту на CRM Бітрікс24 для компанії DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Розробка на базі Бітрікс24 для компанії ТЕХНОТОРГКОМПЛЕКС
    989

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