Bitrix: динамическая стоимость доставки в sale.order.ajax
Как в Bitrix (sale.order.ajax) задать цену доставки: передать зону из JS, посчитать в calculateConcrete и вернуть через setPrice, затем вызвать пересчёт заказа.
Как в Bitrix (sale.order.ajax) динамически установить стоимость доставки при выборе службы и вводе адреса пользователем?
Контекст:
- Создал кастомную службу доставки AddressByZones, в методе calculateConcrete вывожу поле ввода и подсказки (через setDescription):
$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’.
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
- Обновление цены доставки в calculateConcrete (PHP)
- Клиентская логика: передача зоны и пересчёт в sale.order.ajax (JS)
- Пример полной связки: код PHP + JS
- Отладка, типичные ошибки и советы по надёжности
- Источники
- Заключение
Как это работает: 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
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), затем отправляете этот код в форму заказа и триггерите пересчёт. Ниже — надёжный и универсальный пример клиента.
- Установите hidden‑поле в форме заказа (если его нет — создайте). Это гарантирует, что при refreshOrder код зоны придёт на сервер вместе с остальными полями.
- В обработчике выбора подсказки заполняйте это hidden‑поле и запускайте пересчёт через доступную API-функцию или эмуляцию изменения доставки.
Пример JS (поместите в order_ajax.js, адаптируйте селекторы под ваш шаблон):
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.
- В calculateConcrete добавляем hidden и setPrice (см. PHP выше).
- В 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 форматирует её в шаблоне.
Источники
- https://mrcappuccino.ru/blog/post/delivery-handler-for-new-bitrix-sale-module
- https://o2k.ru/blog/raschet-stoimosti-dostavki-po-zonam
- https://blog.budagov.ru/svoy-obrabotchik-dlya-sluzhby-dostavki/
- https://verstaem.com/lessons/making-sale-order-ajax-d7/
- https://dev.1c-bitrix.ru/api_help/sale/events/events_components.php
Заключение
Коротко: для надёжной динамической 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 и корректно повлияет на итоговую сумму.