Автоматическое применение другого типа цены при определенной сумме в корзине

Внимание! Все сообщения на форуме проходят модерацию. Ваше сообщение появится после проверки.
Пару раз находил статьи в интернете, на эту тему. Но постоянно теряются, оставлю тут.
В 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; // опускаем флаг после завершения сохранения
    }
}
Форма ответов
 
Текст сообщения*
Перетащите файлы
Ничего не найдено
Файл
 

Стоимость разработки на 1С-Битрикс:

Индивидуальная разработка магазина

от 350 000 руб. от 5-ти недель

Разработка магазина на 1С-Битрикс с нуля. Дизайн, сборка и оптимизация производительности под конкретный проект и требования. Реализация любого функционала без ограничений готовых решений.

Запуск сайта на готовом решении

от 150 000 руб. от 7-ми дней

Вариант для тех, кто не хочет тратить много средств на индивидуальный проект, и не имеет серьезных требований к сайту. Магазин, быстро запускается на базе одного из 200-та готовых решений.

Мобильное приложение

от 400 000 руб. от 5-ти недель

Разработка кроссплатформенного мобильного приложения, которое не уступает нативным решениям как в производительности, так и пользовательском опыте. Публикуется в AppStore, GooglePlay и RuStore

Сайт компании

от 300 000 руб. от 2-х недель

Корпоративный сайт с информационными разделами, каталогом товаров или услуг. Включает формы обратной связи карточек каталога, любое количество статичных и динамичных разделов.

Инфоресурс

от 300 000 руб. от 4-х недель

Информационный ресурс любой сложности. Сайт для СМИ, городской портал или многопользовательская доска объявлений. Внутренние форумы, блоги- по необходимости.

3D‑моделирование, визуализация

от 25 000 руб. от 3-х дней

По вашим фото, чертежам или описанию создадим 3D‑модели и отрендерим набор изображений для каталога товаров: общий вид, крупные планы и технические ракурсы или 360°‑обзор товара.