Yandex Metrika
sanches.free 1 просмотр

Своя схема скидок в Битрикс: каталог, корзина и обход штатного товарного маркетинга

Зачем выходить за рамки типовых правил

Стандартный товарный маркетинг «1С-Битрикс» хорошо закрывает проценты по товару или по группам каталога. Если же скидка должна зависеть, скажем, от выбранного пункта доставки или от внешнего прайса, проще хранить коэффициенты в своей схеме и применять их в двух местах: при подготовке списка цен в каталоге и при формировании позиции корзины. Так вы не расползаетесь по десятку костыльных правил в админке.

Штатные скидки каталога нужно отключить

Если оставить активные записи в таблицах скидок каталога параллельно с собственной математикой, в части сценариев скидка применится дважды. Перед запуском собственной логики имеет смысл очистить служебные данные (делайте только на копии базы и после бэкапа):

TRUNCATE TABLE b_catalog_discount;
TRUNCATE TABLE b_catalog_discount_cond;

Дальше всю ответственность за величину скидки несёт ваш код.

Выравнивание цен в списке каталога

Введём сервис PricingAdjustmentService с методом, который по массиву элементов из $arResult["ITEMS"] подставляет рассчитанные проценты и актуальные цены до вывода шаблону. Точку вызова выбирайте там, где у вас финализируется $arResult компонента списка (часто result_modifier.php или обёртка вокруг компонента):

$arResult["ITEMS"] = PricingAdjustmentService::applyToCatalogRows($arResult["ITEMS"]);

Внутри сервиса вы читаете свою таблицу или внешнее API и не смешиваете это со встроенным деревом условий.

Подключение логики к корзине

При добавлении товара в корзину укажите собственный класс провайдера данных, чтобы отредактировать цену и скидку до того, как позиция закрепится в b_sale_basket:

Add2BasketByProductID(
    $offerElementId,
    $qty,
    [
        'PRODUCT_PROVIDER_CLASS' => '\Acme\Sale\BasketRowDataProvider',
    ],
    $extraParams
);

Класс BasketRowDataProvider расширяет CCatalogProductProvider: сначала вызываете родительские GetProductData и OrderProduct, затем передаёте результат в свой расчёт и возвращаете изменённый массив полей строки корзины.

class BasketRowDataProvider extends CCatalogProductProvider
{
    public static function GetProductData($arParams)
    {
        $payload = CCatalogProductProvider::GetProductData($arParams);
        $priceRow = CPrice::GetByID((int)$payload['PRODUCT_PRICE_ID']);
        $svc = new PricingAdjustmentService();
        return $svc->decorateBasketPayload($payload, (int)$priceRow['PRODUCT_ID']);
    }

    public static function OrderProduct($arParams)
    {
        $payload = CCatalogProductProvider::OrderProduct($arParams);
        $priceRow = CPrice::GetByID((int)$payload['PRODUCT_PRICE_ID']);
        $svc = new PricingAdjustmentService();
        return $svc->decorateBasketPayload($payload, (int)$priceRow['PRODUCT_ID']);
    }
}

Побочный эффект «пересчёта» между ценами до и после

Ядро пересобирает скидочную сумму из пары значений цены без скидки и после неё; из-за округлений процент может «гулять» на сотые доли по сравнению с тем, что вы показали покупателю в каталоге. Чтобы зафиксировать согласованное значение, сохраните рассчитанный процент в отдельной таблице, ключом сделайте связку пользовательской корзины и товара.

CREATE TABLE basket_line_promo (
  basket_fuser INT UNSIGNED NOT NULL,
  catalog_product_id INT UNSIGNED NOT NULL,
  promo_percent DECIMAL(5,2) NOT NULL,
  PRIMARY KEY (basket_fuser, catalog_product_id)
) ENGINE=InnoDB;

Пример методов работы со снимком (вызовы из decorateBasketPayload, из шаблона корзины и после успешного оформления заказа соответственно):

public function persistLinePromo(int $catalogProductId, float $promoPercent): void
{
    global $DB;
    $basketKey = (int)CSaleBasket::GetBasketUserID();
    $pid = $catalogProductId;
    $pct = sprintf('%.02F', max(0, $promoPercent));
    $sql = sprintf(
        "INSERT INTO basket_line_promo (basket_fuser, catalog_product_id, promo_percent)
         VALUES (%d, %d, '%s')
         ON DUPLICATE KEY UPDATE promo_percent = VALUES(promo_percent)",
        $basketKey,
        $pid,
        $DB->ForSql($pct)
    );
    $DB->Query($sql);
}

public static function readLinePromo(array $cartRow): string
{
    global $DB;
    $basketKey = (int)CSaleBasket::GetBasketUserID();
    $pid = (int)$cartRow['PRODUCT_ID'];
    $sql = sprintf(
        "SELECT promo_percent FROM basket_line_promo
         WHERE basket_fuser = %d AND catalog_product_id = %d",
        $basketKey,
        $pid
    );
    $res = $DB->Query($sql)->Fetch();
    return isset($res['promo_percent']) ? $res['promo_percent'] : '';
}

public static function purgeBasketPromos(): void
{
    global $DB;
    $basketKey = (int)CSaleBasket::GetBasketUserID();
    $DB->Query(sprintf('DELETE FROM basket_line_promo WHERE basket_fuser = %d', $basketKey));
}
  • Сохранение — сразу после того, как в провайдере определён итоговый процент для строки.
  • Чтение — при отрисовке корзины, если нужно вывести тот же процент, что заложили при добавлении.
  • Очистка — после конвертации корзины в заказ или при смене логики, чтобы не тянуть устаревшие значения.

Итог

Своя схема — это отдельное хранилище коэффициентов, единая точка расчёта для каталога и корзины, кастомный PRODUCT_PROVIDER_CLASS и, при необходимости, вспомогательная таблица для защиты от дрожания округлений. Такой каркас проще покрыть тестами и сопровождать, чем наращивать ветвление в типовых правилах маркетинга.

Не хотите копаться сами?

Починю за 1-3 дня. Без предоплаты — оплата по результату.

15+ лет опыта с 1С-Битрикс · Без предоплаты · 7 дней гарантии