Своя схема скидок в Битрикс: каталог, корзина и обход штатного товарного маркетинга
Зачем выходить за рамки типовых правил
Стандартный товарный маркетинг «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 дней гарантии