Перейти к содержимому

Свой расчёт в оформлении заказа - цена позиции и доставка

Считаем свои цены и свою доставку так, чтобы они пережили пересчёт и доехали до заказа в неизменном виде.

Механика

Оформление заказа устроено как непрерывный пересчёт, и это главное, что стоит понять. Любое изменение формы уходит на сервер, где заказ собирается заново: корзина, скидки, доставка, оплата и итог. Всё, что нарисовано в шаблоне или в браузере, живёт ровно до первого такого пересчёта.

Цена позиции приходит не из шаблона, а от провайдера продукта. Правка в result_modifier.php меняет только показ на странице, а в заказ уезжает цена из каталога, и обнаруживается это уже в админке.

Честных путей всего два, и выбирают между ними по гибкости. Свой провайдер продукта считает цену каждый раз заново по своей логике. Явная цена позиции с признаком собственной цены проще, но выключает пересчёт этой строки.

Явная цена требует заполнить руками то, что обычно приходит само. Название, адрес карточки и внешний код товара при своей цене задают в полях позиции, иначе менеджер увидит в заказе строку без имени и без ссылки.

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

Данные с фронта доезжают до сервера свойством заказа. Адрес, этаж или зону кладут в поле свойства, а расчёт читает их из коллекции свойств уже на сервере, где им и место.

Внешний расчёт вызывают на сервере в момент пересчёта, а не в шаблоне страницы. Ответ внешней системы кладут в цену позиции или в скидку, и только тогда он переживает сохранение заказа.

Шаги

  1. Решить, где живёт расчёт: провайдер продукта, явная цена или скидка.
  2. Написать провайдер либо задать цену позиции вместе с обязательными полями.
  3. Данные покупателя принимать свойством заказа, а не своим полем формы.
  4. Стоимость доставки считать в своей службе, на сервере, а не в браузере.
  5. Проверить, что цена пережила пересчёт при смене доставки и оплаты.
  6. Открыть готовый заказ в админке: имя позиции, ссылка, цена, свойства.

Код

Задаём цену позиции явно:

$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 меняет только показ, а цена приходит от провайдера продукта. Нужен свой провайдер либо явная цена позиции.

Цена доставки обнулилась после смены оплаты.

Стоимость подставили в разметке, а платформа пересчитала доставку заново. Расчёт живёт в методе службы доставки на сервере.

Своё поле формы не сохранилось в заказе.

Модуль продаж принимает только известные ему свойства своего типа плательщика. Поле заводят свойством и отдают под его именем.

В админке у позиции нет имени и ссылки на товар.

При собственной цене название, адрес карточки и внешний код заполняются вручную. Штатный провайдер эти поля больше не подставляет.

Промокод не действует на часть позиций.

У этих позиций выставлен признак собственной цены, и правила корзины их не трогают. Скидку в таком случае считают своим кодом.

Оформление стало открываться по нескольку секунд.

Внешний расчёт вызывается на каждом пересчёте формы без кэша и таймаута. Ответ кэшируют, а ожидание ограничивают.

Частые вопросы

Как менять цены на товары в оформлении заказа?

Своим провайдером продукта или явной ценой позиции с признаком собственной цены. Правка цены в шаблоне меняет только показ: при сохранении заказа платформа возьмёт цену из каталога.

Как посчитать доставку по своей логике?

В методе расчёта своей службы доставки, на сервере. Фронт только собирает адрес или зону в свойство заказа и просит пересчёт, иначе цена теряется при любом изменении формы.

Чем провайдер лучше собственной цены позиции?

Он считает цену заново при каждом пересчёте и не выключает скидки. Явная цена проще, но такая позиция перестаёт участвовать в правилах корзины и купонах.

Почему в админке позиция без названия и ссылки?

При собственной цене поля названия, адреса карточки и внешнего кода заполняются вручную. Штатный провайдер их больше не подставляет, а менеджеру они нужны каждый день.

Как проверить, что расчёт доехал до заказа?

Загрузить сохранённый заказ и сравнить цены позиций и итог с тем, что показывала страница. Проверка по витрине ничего не доказывает: там показан результат работы шаблона.

Смежное

Первоисточник