Свой расчёт в оформлении заказа - цена позиции и доставка
Считаем свои цены и свою доставку так, чтобы они пережили пересчёт и доехали до заказа в неизменном виде.
Механика
Оформление заказа устроено как непрерывный пересчёт, и это главное, что стоит понять. Любое изменение формы уходит на сервер, где заказ собирается заново: корзина, скидки, доставка, оплата и итог. Всё, что нарисовано в шаблоне или в браузере, живёт ровно до первого такого пересчёта.
Цена позиции приходит не из шаблона, а от провайдера продукта. Правка в
result_modifier.php меняет только показ на странице, а в заказ уезжает цена из
каталога, и обнаруживается это уже в админке.
Честных путей всего два, и выбирают между ними по гибкости. Свой провайдер продукта считает цену каждый раз заново по своей логике. Явная цена позиции с признаком собственной цены проще, но выключает пересчёт этой строки.
Явная цена требует заполнить руками то, что обычно приходит само. Название, адрес карточки и внешний код товара при своей цене задают в полях позиции, иначе менеджер увидит в заказе строку без имени и без ссылки.
Стоимость доставки считает служба доставки, а не форма и не браузер. Расчёт живёт в методе службы, который платформа вызывает при каждом пересчёте заказа, и подставленная в разметку цена теряется на первом же обращении к серверу.
Данные с фронта доезжают до сервера свойством заказа. Адрес, этаж или зону кладут в поле свойства, а расчёт читает их из коллекции свойств уже на сервере, где им и место.
Внешний расчёт вызывают на сервере в момент пересчёта, а не в шаблоне страницы. Ответ внешней системы кладут в цену позиции или в скидку, и только тогда он переживает сохранение заказа.
Шаги
- Решить, где живёт расчёт: провайдер продукта, явная цена или скидка.
- Написать провайдер либо задать цену позиции вместе с обязательными полями.
- Данные покупателя принимать свойством заказа, а не своим полем формы.
- Стоимость доставки считать в своей службе, на сервере, а не в браузере.
- Проверить, что цена пережила пересчёт при смене доставки и оплаты.
- Открыть готовый заказ в админке: имя позиции, ссылка, цена, свойства.
Код
Задаём цену позиции явно:
$item = $basket->createItem('catalog', $productId);$item->setFields([ 'QUANTITY' => 1, 'CURRENCY' => 'RUB', 'LID' => SITE_ID, 'CUSTOM_PRICE' => 'Y', 'PRICE' => 990, 'BASE_PRICE' => 1290, 'NAME' => $name, 'DETAIL_PAGE_URL' => $url, 'PRODUCT_XML_ID' => $xmlId,]);$basket->save();// без трёх последних полей позиция в админке будет без имени и без ссылки// базовую цену задают рядом: из неё считается показанная покупателю скидкаПризнак собственной цены говорит платформе не трогать эту строку при пересчёте. Путь простой и рабочий, но ограниченный: правила корзины и купоны такую позицию больше не видят.
Наследуем провайдер продукта:
class VendorProvider extends \CCatalogProductProvider{ public static function GetProductData($params) { $data = parent::GetProductData($params); // остальное берём штатным $data['PRICE'] = self::contractPrice($params['PRODUCT_ID'], $params['USER_ID']); return $data; }}// провайдер отвечает и за остаток: методы количества наследуются как естьПровайдер продукта отвечает далеко не только за одну цену позиции. Через него же идут проверка количества, резервирование и списание со склада, поэтому родительский метод вызывают всегда, а правят только нужное поле.
Привязываем провайдер к позиции корзины:
$item->setField('PRODUCT_PROVIDER_CLASS', VendorProvider::class);$basket->save();// имя класса хранится в самой позиции, поэтому старые заказы не меняютсяПровайдер задаётся строкой в позиции корзины и живёт вместе с ней. Уже оформленные заказы продолжают считаться прежним классом, и это скорее плюс: история цен не переписывается задним числом.
Отдаём данные с фронта свойством заказа:
// поле уезжает на сервер под именем свойства заказаdocument.querySelector('[name="ORDER_PROP_15"]').value = address;// дальше форма штатно просит сервер пересчитать заказ целикомСвоё поле, дописанное в разметку мимо свойств заказа, модуль продаж просто не примет. Поэтому адрес, зону или этаж заводят свойством заказа: у значения появляется хранение, показ менеджеру и доступ из любого расчёта.
Читаем свойство в расчёте доставки:
$order = $shipment->getCollection()->getOrder();$floor = $order->getPropertyCollection()->getItemByOrderPropertyId(15)->getValue();// на сервере доступен весь заказ: корзина, покупатель, местоположение// номер свойства держат в настройке модуля, а не числом в коде расчётаСчитаем стоимость доставки:
protected function calculateConcrete(\Bitrix\Sale\Shipment $shipment){ $result = new \Bitrix\Sale\Delivery\CalculationResult(); $result->setDeliveryPrice(300 + 50 * (int)$this->floorFrom($shipment)); return $result; // платформа вызывает метод при каждом пересчёте заказа}// отказ внешнего сервиса возвращают ошибкой результата, а не исключениемМетод расчёта вызывается заново при смене корзины, адреса и оплаты. Цена, посчитанная один раз в браузере, до заказа не доживает, а посчитанная здесь - попадает и в заказ, и в письмо, и в чек.
Сверяем цены после пересчёта:
foreach ($order->getBasket() as $item) { printf("%-30s база %8s цена %8s своя %s\n", $item->getField('NAME'), $item->getBasePrice(), $item->getPrice(), $item->getField('CUSTOM_PRICE'));}// то же самое видно в админке заказа, на вкладке состава корзиныСверку делают не глазами по витрине, а по данным сохранённого в базе заказа. Витрина показывает то, что посчитал шаблон, а заказ - то, что сохранила платформа, и расходятся они именно в этом месте.
Открываем сохранённый заказ:
$order = \Bitrix\Sale\Order::load($orderId);echo $order->getPrice(); // итог, который увидит менеджер и платёжная системаecho $order->getBasket()->getPrice(); // сумма позиций без доставки и налогов// расхождение этих двух чисел объясняется доставкой, скидкой или налогомИтог заказа считается из позиций и доставки после всех пересчётов. Именно это число уходит в платёжную систему и в кассовый чек, поэтому проверять стоит его, а не сумму на странице оформления.
Ограничения
Собственная цена позиции выключает скидки на неё. Правила корзины, купоны и акции такую строку не пересчитывают, и покупатель с промокодом получит не ту сумму, которую ждёт. Это осознанный обмен простоты на гибкость, а не побочный эффект настройки.
Провайдер продукта отвечает за целую группу задач. Кроме цены через него идут проверка остатка, резервирование и списание, поэтому наследование без вызова родительских методов ломает склад тише, чем кажется.
Внешний расчёт вызывается на каждом пересчёте формы. Оформление обращается к серверу по десятку раз за сессию, поэтому внешнему вызову задают таймаут и кэш, иначе чужой сервис превращает оформление в ожидание.
Менеджер меняет заказ руками в админке. Позиция со своей ценой сохранит её при правке, а позиция с провайдером будет пересчитана заново, и это стоит объяснить отделу продаж до запуска.
Расхождение витрины и заказа доедет до чека. Онлайн-касса берёт цены из позиций заказа, поэтому «на сайте было дешевле» превращается в расхождение документов, а не в спор о вёрстке.
Типичные проблемы
Цена, посчитанная в шаблоне, в заказ не попала.
Правка в result_modifier меняет только показ, а цена приходит от провайдера продукта. Нужен свой провайдер либо явная цена позиции.
Цена доставки обнулилась после смены оплаты.
Стоимость подставили в разметке, а платформа пересчитала доставку заново. Расчёт живёт в методе службы доставки на сервере.
Своё поле формы не сохранилось в заказе.
Модуль продаж принимает только известные ему свойства своего типа плательщика. Поле заводят свойством и отдают под его именем.
В админке у позиции нет имени и ссылки на товар.
При собственной цене название, адрес карточки и внешний код заполняются вручную. Штатный провайдер эти поля больше не подставляет.
Промокод не действует на часть позиций.
У этих позиций выставлен признак собственной цены, и правила корзины их не трогают. Скидку в таком случае считают своим кодом.
Оформление стало открываться по нескольку секунд.
Внешний расчёт вызывается на каждом пересчёте формы без кэша и таймаута. Ответ кэшируют, а ожидание ограничивают.
Частые вопросы
Как менять цены на товары в оформлении заказа?
Своим провайдером продукта или явной ценой позиции с признаком собственной цены. Правка цены в шаблоне меняет только показ: при сохранении заказа платформа возьмёт цену из каталога.
Как посчитать доставку по своей логике?
В методе расчёта своей службы доставки, на сервере. Фронт только собирает адрес или зону в свойство заказа и просит пересчёт, иначе цена теряется при любом изменении формы.
Чем провайдер лучше собственной цены позиции?
Он считает цену заново при каждом пересчёте и не выключает скидки. Явная цена проще, но такая позиция перестаёт участвовать в правилах корзины и купонах.
Почему в админке позиция без названия и ссылки?
При собственной цене поля названия, адреса карточки и внешнего кода заполняются вручную. Штатный провайдер их больше не подставляет, а менеджеру они нужны каждый день.
Как проверить, что расчёт доехал до заказа?
Загрузить сохранённый заказ и сравнить цены позиций и итог с тем, что показывала страница. Проверка по витрине ничего не доказывает: там показан результат работы шаблона.
Смежное
-
Оформление заказа - оглавление подтемы
-
Оформление заказа: настройка компонента и правка шаблона - параметры компонента и свойства
-
События оформления заказа: свои проверки, запрет и допданные - где стоят серверные проверки
-
Своя служба доставки: расчёт, ограничения, внешний API - устройство самой службы
-
Работа с корзиной из кода: позиции, количество, пересчёт - что приезжает в оформление
-
Каталог и продажи - устройство магазина целиком
-
Дополнительные услуги в заказе: упаковка, сборка, страховка - платные услуги в сумме заказа