Веб

Bitrix: динамическая стоимость доставки в sale.order.ajax

Как в Bitrix (sale.order.ajax) задать цену доставки: передать зону из JS, посчитать в calculateConcrete и вернуть через setPrice, затем вызвать пересчёт заказа.

1 ответ 5 просмотров

Как в Bitrix (sale.order.ajax) динамически установить стоимость доставки при выборе службы и вводе адреса пользователем?

Контекст:

  • Создал кастомную службу доставки AddressByZones, в методе calculateConcrete вывожу поле ввода и подсказки (через setDescription):
php
$description = '
 <div class="form-floating mb-3" style="position:relative;">
 <input class="form-control" type="text" id="delivery-address-input" placeholder="Введите адрес" autocomplete="off">
 <ul id="delivery-address-suggest" style="border:1px solid #ccc; z-index: 11; max-width:300px; position:absolute; background:#fff; list-style:none; margin:0; padding:0;"></ul>
 </div>
 ';

$result->setDescription($description);
  • В order_ajax.js на событии onAjaxSuccess при выборе доставки (ID = 765) подключаю Yandex Suggest, показываю подсказки и при выборе адреса отправляю его POST на /bitrix/tools/AddressByZonesGetCoordinate.php. В обработчике получаю код зоны, например ‘SPB_A’.
javascript
BX.addCustomEvent('onAjaxSuccess', function () {
 var selectedDeliveryId = document.querySelector('input[name="DELIVERY_ID"]:checked').value;
 if (selectedDeliveryId == 765) {
 // ...инициализация Yandex Suggest и подсказок...

 // при клике на подсказку
 const response = await fetch('/bitrix/tools/AddressByZonesGetCoordinate.php', {
 method: 'POST',
 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 body: encodeURIComponent(input.value)
 });
 const result = await response.text(); // например 'SPB_A'
 }
});

Вопрос:

Каким образом в этой связке (calculateConcrete + клиентский JS) правильно и надёжно установить стоимость доставки в оформлении заказа (sale.order.ajax) на основе выбранного адреса/зоны (например установить цену 222), чтобы она отобразилась в checkout и изменила итоговую сумму заказа?

Прошу привести конкретные рекомендации и примеры кода (PHP/JS):

  • где и как передавать вычисленную цену (в calculateConcrete через \Bitrix\Sale\Delivery\CalculationResult, через getShipmentExtraServices, через API sale.order.ajax)?
  • нужно ли триггерить пересчёт заказа и каким способом (какой AJAX-вызов/событие вызывать)?
  • как корректно обновить интерфейс checkout после установки цены?

Может, кто-то уже реализовывал похожую логику — поделитесь рабочим примером и лучшей практикой для интеграции с sale.order.ajax.

В Bitrix доставка через sale.order.ajax динамическую цену лучше устанавливать на сервере в методе calculateConcrete — вернуть цену через $result->setPrice($price) и передать код зоны из фронта (hidden‑поле или POST). После выбора подсказки на клиенте установите значение ADDRESS_ZONE и вызовите пересчёт заказа (например BX.Sale.OrderAjax.calculateDelivery() или BX.sale.order.ajax.recalculate(); альтернативно — эмуляция change на input[name="DELIVERY_ID"] или ручной POST к компоненту). Так цена (например 222) попадёт в checkout и изменит итоговую сумму.


Содержание


Как это работает: Bitrix доставка и роль calculateConcrete

Когда sale.order.ajax делает «пересчёт» корзины, компонент посылает AJAX‑запрос на сервер, где Bitrix вызывает ваши обработчики доставки — в частности метод calculateConcrete вашей службы. В этом методе вы формируете объект \Bitrix\Sale\Delivery\CalculationResult и задаёте итоговую цену через $result->setPrice($price). После возврата результата компонент обновит интерфейс и итоговые суммы на странице (пример использования setPrice и поведения — в практике разработчиков: https://mrcappuccino.ru/blog/post/delivery-handler-for-new-bitrix-sale-module, также см. пример пересчёта в JS: https://verstaem.com/lessons/making-sale-order-ajax-d7/).

Откуда сервер возьмёт информацию о зоне/адресе? Обычно фронт передаёт её в запросе refresh (все поля формы отправляются). Значит, нужно положить код зоны (например “SPB_A”) в скрытое поле формы (например name=“ADDRESS_ZONE”) или отправить как часть POST при вызове refresh — тогда calculateConcrete сможет прочитать его через \Bitrix\Main\Context::getCurrent()->getRequest()->getPost('ADDRESS_ZONE') и посчитать цену.


Обновление цены доставки в calculateConcrete (PHP)

Ниже — рабочий шаблон для calculateConcrete: читаем код зоны из POST (или из свойства заказа), вычисляем цену и возвращаем её через CalculationResult. Обратите внимание: важно возвращать число (float) и добавлять скрытое поле в описание (чтобы при следующем refresh значение присутствовало в POST).

php
<?php
use Bitrix\Main\Context;
use Bitrix\Sale\Delivery\CalculationResult;

public static function calculateConcrete(\Bitrix\Sale\Shipment $shipment, array $extra = array())
{
 $result = new CalculationResult();

 // Получаем POST-параметр (sale.order.ajax отправляет все поля формы при refresh)
 $request = Context::getCurrent()->getRequest();
 $zone = trim((string)$request->getPost('ADDRESS_ZONE'));

 // Если zone отсутствует в POST, можно попытаться вычислить из свойства заказа
 if ($zone === '') {
 $order = $shipment->getOrder();
 $propertyCollection = $order->getPropertyCollection();
 // Замените YOUR_ADDRESS_PROPERTY_CODE на код вашего свойства адреса
 $addressProp = $propertyCollection->getItemByOrderPropertyId(YOUR_ADDRESS_PROPERTY_ID);
 if ($addressProp) {
 $addressValue = (string)$addressProp->getValue();
 // Здесь можно преобразовать addressValue -> zone (например через гео/DaData)
 $zone = self::getZoneByAddressString($addressValue);
 }
 }

 // Пример: простой маппинг зон -> цены
 $price = (float) self::getPriceByZone($zone); // реализуйте сами логику getPriceByZone

 // Возвращаем цену доставки
 $result->setPrice($price);

 // Вставляем в описание скрытое поле, чтобы оно было в форме при следующем refresh
 $hidden = '<input type="hidden" name="ADDRESS_ZONE" id="ADDRESS_ZONE_765" value="'.htmlspecialcharsbx($zone).'" />';
 $description = $hidden . '<div>Доставка по зоне: '.htmlspecialcharsbx($zone).'</div>';
 $result->setDescription($description);

 return $result;
}

protected static function getPriceByZone($zone)
{
 $map = [
 'SPB_A' => 222,
 'MOS_A' => 300,
 ];
 return isset($map[$zone]) ? $map[$zone] : 0;
}

Пояснения и рекомендации:

  • Используйте $result->setPrice() как основной механизм установки итоговой стоимости доставки — это штатный подход (см. примеры: https://mrcappuccino.ru/blog/post/delivery-handler-for-new-bitrix-sale-module, https://o2k.ru/blog/raschet-stoimosti-dostavki-po-zonam).
  • Не пытайтесь «править» DOM‑тексты цены только на клиенте — при следующем сохранении/пересчёте серверная цена перезапишет значения.
  • Если у вас есть дополнительные платные опции (extra services), их можно вернуть через $result->setShipmentExtraServices($services), но для базовой цены это лишнее.

Клиентская логика: отправка зоны и пересчёт в sale.order.ajax (JS)

Идея проста: после выбора подсказки вы получаете код зоны (у вас — /bitrix/tools/AddressByZonesGetCoordinate.php), затем отправляете этот код в форму заказа и триггерите пересчёт. Ниже — надёжный и универсальный пример клиента.

  1. Установите hidden‑поле в форме заказа (если его нет — создайте). Это гарантирует, что при refreshOrder код зоны придёт на сервер вместе с остальными полями.
  2. В обработчике выбора подсказки заполняйте это hidden‑поле и запускайте пересчёт через доступную API-функцию или эмуляцию изменения доставки.

Пример JS (поместите в order_ajax.js, адаптируйте селекторы под ваш шаблон):

javascript
BX.addCustomEvent('onAjaxSuccess', function() {
 // Выполняется после перерисовки sale.order.ajax — удобное место для (re)инициализации подсказок.
 var deliveryId = '765';
 var deliveryRadio = document.querySelector('input[name="DELIVERY_ID"][value="' + deliveryId + '"]');
 if (!deliveryRadio) return;

 // Инициализация Yandex Suggest и подписка на выбор — пропущена, в вашем коде она уже есть.
 // Предположим, при выборе адреса вы вызываете функцию onAddressChosen(addressValue).

 window.onAddressChosen = async function(addressValue) {
 // 1) Запрашиваем зону
 var resp = await fetch('/bitrix/tools/AddressByZonesGetCoordinate.php', {
 method: 'POST',
 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 body: 'address=' + encodeURIComponent(addressValue)
 });
 var zone = (await resp.text()).trim();
 if (!zone) return;

 // 2) Устанавливаем hidden-поле в форме заказа (идентификатор формы может отличаться)
 var orderForm = document.querySelector('#bx-soa-order-form') || document.forms['ORDER_FORM'];
 if (!orderForm) {
 console.warn('Order form not found. Adjust selector for your template.');
 return;
 }
 var hidden = orderForm.querySelector('input[name="ADDRESS_ZONE"]');
 if (!hidden) {
 hidden = document.createElement('input');
 hidden.type = 'hidden';
 hidden.name = 'ADDRESS_ZONE';
 hidden.id = 'ADDRESS_ZONE_' + deliveryId;
 orderForm.appendChild(hidden);
 }
 hidden.value = zone;

 // 3) Триггерим пересчёт — пробуем штатные API, затем fallback
 // Первый вариант (часто доступен в новых шаблонах):
 if (window.BX && BX.Sale && typeof BX.Sale.OrderAjax === 'object' && typeof BX.Sale.OrderAjax.calculateDelivery === 'function') {
 BX.Sale.OrderAjax.calculateDelivery(); // иногда просто вызов без аргументов
 return;
 }
 // Второй вариант (встречается в других сборках):
 if (window.BX && BX.sale && BX.sale.order && BX.sale.order.ajax && typeof BX.sale.order.ajax.recalculate === 'function') {
 BX.sale.order.ajax.recalculate();
 return;
 }
 // Третий вариант: эмулируем событие change на input delivery — компоненты обычно слушают это событие
 if (deliveryRadio) {
 deliveryRadio.checked = true;
 if (window.BX && typeof BX.fireEvent === 'function') {
 BX.fireEvent(deliveryRadio, 'change');
 } else {
 deliveryRadio.dispatchEvent(new Event('change', { bubbles: true }));
 }
 return;
 }

 // Четвёртый — ручной POST к ajax.php компонента (fallback). Формат ответа шаблона может отличаться.
 try {
 var fd = new FormData(orderForm);
 fd.set('ADDRESS_ZONE', zone);
 fd.set('action', 'refreshOrder');
 if (typeof BX !== 'undefined' && BX.bitrix_sessid) {
 fd.set('sessid', BX.bitrix_sessid());
 }
 var r = await fetch('/bitrix/components/bitrix/sale.order.ajax/ajax.php', {
 method: 'POST',
 credentials: 'same-origin',
 body: fd
 });
 var json = await r.json();
 // Варианты ответа зависят от шаблона; попробуйте посмотреть network -> ответ и заменить нужную часть DOM.
 if (json && json.TEMPLATE_DATA && json.TEMPLATE_DATA.ORDER_HTML) {
 document.querySelector('#bx-soa-order').innerHTML = json.TEMPLATE_DATA.ORDER_HTML;
 } else if (json && json.HTML) {
 document.querySelector('#bx-soa-order').innerHTML = json.HTML;
 } else {
 console.warn('Неизвестный формат ответа sale.order.ajax', json);
 }
 } catch (e) {
 console.error('Fallback refresh failed', e);
 }
 };
});

Ключевые замечания:

  • Обычно достаточно заполнить hidden input и вызвать внутренний пересчёт — компонент сам подставит поле в POST и сервер прочитает ADDRESS_ZONE.
  • Конкретные имена функций в глобальной области зависят от версии/шаблона — в разных проектах это BX.Sale.OrderAjax.calculateDelivery() или BX.sale.order.ajax.recalculate(); проверяйте наличие функций в консоли (см. пример на практике: https://verstaem.com/lessons/making-sale-order-ajax-d7/).
  • Если вы используете динамическую подгрузку блока доставки (onAjaxSuccess), не забудьте повторно инициализировать подсказки и обработчики после каждой перерисовки.

Пример полной связки: PHP + JS

Сценарий: пользователь выбирает подсказку, фронт получает ‘SPB_A’, вы хотите сразу увидеть цену 222 в checkout.

  1. В calculateConcrete добавляем hidden и setPrice (см. PHP выше).
  2. В order_ajax.js при выборе подсказки делаем fetch к /bitrix/tools/AddressByZonesGetCoordinate.php, получаем ‘SPB_A’, кладём в hidden ADDRESS_ZONE и вызываем пересчёт через API или эмуляцию change.

Пример последовательности (сокращённо):

  • Пользователь выбрал подсказку → JS: fetch(‘/bitrix/tools/AddressByZonesGetCoordinate.php’, address) → ответ ‘SPB_A’
  • JS: document.querySelector(‘input[name=“ADDRESS_ZONE”]’).value = ‘SPB_A’
  • JS: BX.Sale.OrderAjax.calculateDelivery() (или dispatch change)
  • Компонент отправляет AJAX на сервер → в calculateConcrete читаем ADDRESS_ZONE и делаем $result->setPrice(222)
  • Сервер возвращает пересчитанный HTML/JSON → checkout обновляет суммы.

Если что-то не обновилось — смотрите network: какие поля были в POST; именно туда должен попасть ADDRESS_ZONE. При ручной отладке полезно логировать входящие POST в /bitrix/tools/AddressByZonesGetCoordinate.php и в calculateConcrete.


Отладка, типичные ошибки и советы по надёжности

  • Hidden поле не попадает в POST: убедитесь, что поле находится внутри формы sale.order.ajax (обычно #bx-soa-order-form). Если вы добавляете поле в описание службы доставки, проверьте что шаблон включает описание внутрь формы; иначе добавляйте hidden программно в форму.
  • Функция пересчёта недоступна: проверьте в консоли наличие BX.Sale.OrderAjax или BX.sale.order.ajax. Поведение зависит от версии Bitrix и шаблона. Используйте несколько fallback‑методов (API → emulate change → manual POST).
  • Асинхронность: onAjaxSuccess перезаписывает DOM — привязки должны реинициализироваться. Привязывайтесь через BX.addCustomEvent(‘onAjaxSuccess’, …) (как вы уже делаете).
  • Безопасность: валидируйте и фильтруйте код зоны на сервере. Не доверяйте напрямую клиентским значениям.
  • Кэширование: если вы кэшируете расчёт доставки где‑то ещё — инвалидируйте кэш при изменении входных параметров.
  • Гонки: если пользователь быстро меняет адрес — поставьте debounce (200–500ms) перед fetch и пересчётом.
  • Локализация/валюта: возвращаемая цена должна соответствовать валюте заказа; Bitrix форматирует её в шаблоне.

Источники


Заключение

Коротко: для надёжной динамической Bitrix доставка через sale.order.ajax вычисляйте цену на сервере в calculateConcrete ($result->setPrice($price)), передавайте код зоны в POST (hidden input ADDRESS_ZONE или параметр при refresh) и инициируйте пересчёт на клиенте (BX.Sale.OrderAjax.calculateDelivery() / BX.sale.order.ajax.recalculate() или эмуляция change). Такой подход гарантирует, что цена (например 222) отобразится в checkout и корректно повлияет на итоговую сумму.

Авторы
Проверено модерацией
Модерация
Bitrix: динамическая стоимость доставки в sale.order.ajax