В init.php, пояснения в комментах
| Код |
|---|
/<?php
/**
* Файл: setOptimalPrice.php
* Назначение: автоматически применять оптовую цену (тип цены ID=4) ко всем
* товарам в корзине, если общая сумма корзины достигает 10 000 руб.
* Если сумма опускается ниже порога — цены возвращаются к базовым (ID=2).
*
* Подключается через local/php_interface/init.php автоматически,
* т.к. тот загружает все файлы из папки handlers/ через glob().
*
* Типы цен (Магазин → Настройки → Типы цен):
* ID=2 — базовая (розничная) цена, задана у всех товаров
* ID=4 — оптовая цена, задана только у части товаров
*/
// Подписываемся на событие OnSaleBasketSaved.
// Оно срабатывает ПОСЛЕ того, как корзина успешно сохранена в БД
// (при добавлении товара, изменении количества, удалении позиции).
// Важно: используем именно это событие, а не OnBeforeBasketAdd/Update,
// чтобы не вмешиваться в транзакцию сохранения и не вызвать рекурсию.
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleBasketSaved',
'applyWholesalePriceIfNeeded'
);
/**
* Вспомогательная функция: получает цену товара по ID типа цены.
*
* @param int $productId ID товара или торгового предложения (SKU)
* @param int $groupId ID типа цены (2 — базовая, 4 — оптовая)
* @return float|null Цена в рублях, или null если такой цены нет у товара
*/
function getPriceByGroup(int $productId, int $groupId): ?float
{
// CPrice::GetList — стандартный метод Битрикс для получения цен из каталога.
// Первый аргумент — сортировка (не нужна), второй — фильтр.
$dbPrice = \CPrice::GetList(
[],
[
'PRODUCT_ID' => $productId,
'CATALOG_GROUP_ID' => $groupId,
]
);
if ($arPrice = $dbPrice->Fetch()) {
return (float)$arPrice['PRICE'];
}
// Если запись не найдена — у товара нет цены данного типа
return null;
}
/**
* Основной обработчик события OnSaleBasketSaved.
* Загружает корзину текущего пользователя, считает сумму и,
* в зависимости от порога 10 000 руб., проставляет нужный тип цены.
*
* @param \Bitrix\Main\Event $event Объект события (не используется напрямую,
* но требуется сигнатурой обработчика D7)
*/
function applyWholesalePriceIfNeeded(\Bitrix\Main\Event $event): void
{
// Решение №3: пропускаем выполнение во время AJAX-запросов компонента
// оформления заказа (refreshOrderAjax, saveOrderAjax и т.д.).
// В этот момент ядро само пересчитывает корзину через refreshData(),
// что вызывает многократные срабатывания события с непредсказуемым
// состоянием цен. Цены будут актуально выставлены при следующем
// реальном изменении корзины пользователем.
$request = \Bitrix\Main\Context::getCurrent()->getRequest();
if (
$request->isPost()
&& $request->getPost('via_ajax') === 'Y'
&& $request->getPost('soa-action') !== null
) {
return;
}
// Статический флаг защиты от рекурсии.
// Проблема: внутри функции мы сами вызываем $basket->save(),
// что снова триггерит OnSaleBasketSaved → снова вызывает эту функцию.
// Флаг разрывает этот бесконечный цикл.
static $isProcessing = false;
if ($isProcessing) {
return;
}
// Загружаем корзину текущего пользователя (FUser — анонимный или авторизованный).
// Fuser::getId() возвращает ID записи в таблице b_sale_fuser для текущей сессии.
$basket = \Bitrix\Sale\Basket::loadItemsForFUser(
\Bitrix\Sale\Fuser::getId(),
\Bitrix\Main\Context::getCurrent()->getSite() // код сайта, например 's1'
);
// Если корзина пуста или не загрузилась — нечего пересчитывать
if (!$basket || $basket->count() === 0) {
return;
}
// Решение №2: считаем total по базовым каталожным ценам (ID=2), а не по
// текущим ценам в корзине. Это защищает от ситуации, когда ядро Битрикс
// в момент события уже сбросило цены через refreshData() и $basketItem->getPrice()
// возвращает промежуточное (ненадёжное) значение.
// Базовые цены из БД каталога — всегда стабильны и не зависят от состояния
// объекта корзины в памяти.
$total = 0;
foreach ($basket as $basketItem) {
$basePrice = getPriceByGroup($basketItem->getProductId(), 2);
$total += ($basePrice ?? $basketItem->getPrice()) * $basketItem->getQuantity();
}
// Определяем, применять ли оптовые цены.
// $total посчитан по базовым каталожным ценам — результат всегда стабилен.
$useWholesale = ($total >= 10000);
// Флаг: были ли фактические изменения цен.
// Сохранять корзину имеет смысл только если что-то поменялось,
// чтобы не создавать лишние запросы к БД.
$needSave = false;
foreach ($basket as $basketItem) {
$productId = $basketItem->getProductId(); // ID товара / торгового предложения
if ($useWholesale) {
// Сумма корзины >= 10 000 — пробуем применить оптовую цену
$wholesalePrice = getPriceByGroup($productId, 4);
if ($wholesalePrice !== null) {
// Оптовая цена у товара есть — проверяем, отличается ли она от текущей.
// Сравниваем с погрешностью 0.001, чтобы избежать бесконечного цикла
// из-за погрешностей float-арифметики.
if (abs($basketItem->getPrice() - $wholesalePrice) > 0.001) {
$basketItem->setField('PRICE', $wholesalePrice);
// CUSTOM_PRICE=Y говорит Битриксу не перезаписывать цену
// автоматически при следующем пересчёте каталожных цен
$basketItem->setField('CUSTOM_PRICE', 'Y');
$needSave = true;
}
}
// Если оптовой цены у товара нет — оставляем текущую цену без изменений
} else {
// Сумма корзины < 10 000 — возвращаем базовую (розничную) цену
$basePrice = getPriceByGroup($productId, 2);
if ($basePrice !== null) {
// Базовая цена найдена — проверяем, нужно ли обновлять
if (abs($basketItem->getPrice() - $basePrice) > 0.001) {
$basketItem->setField('PRICE', $basePrice);
$basketItem->setField('CUSTOM_PRICE', 'Y');
$needSave = true;
}
}
}
}
// Сохраняем корзину только если реально изменили хотя бы одну цену
if ($needSave) {
$isProcessing = true; // поднимаем флаг ДО save(), чтобы заблокировать рекурсию
$basket->save();
$isProcessing = false; // опускаем флаг после завершения сохранения
}
}
|