Обработчик доставки для sale в Битрикс D7
Обработчик доставки в sale 16+
В актуальных ветках модуля торговли службы описывают классы, наследующие \Bitrix\Sale\Delivery\Services\Base. Инфраструктурные заготовки смотрите в /bitrix/modules/sale/handlers/delivery/. Подключение через /local/php_interface/include/sale_delivery/ на сборках семейства 16.x может не подхватиться; надёжный путь — зарегистрировать свой класс в списке обработчиков событием onSaleDeliveryHandlersClassNamesBuildList.
Регистрация файла класса
Событие ждёт сопоставление «полное имя класса → абсолютный путь от корня сайта». Совместите путь с автозагрузкой (composer.json в local, собственное PSR‑4 или аккуратный require_once).
use Bitrix\Main\Event;
use Bitrix\Main\EventManager;
use Bitrix\Main\EventResult;
EventManager::getInstance()->addEventHandler(
'sale',
'onSaleDeliveryHandlersClassNamesBuildList',
static function (Event $event): EventResult {
return new EventResult(EventResult::SUCCESS, [
\Logistics\VioletRoute\HarborPostalHub::class
=> '/local/lib/Logistics/VioletRoute/HarborPostalHub.php',
\Logistics\VioletRoute\HarborPostalRushProfile::class
=> '/local/lib/Logistics/VioletRoute/HarborPostalRushProfile.php',
]);
}
);Родитель с профилями
Заголовки и описание для администратора задают статические методы getClassTitle и getClassDescription; флаг мгновенного пересчёта — переопределение isCalculatePriceImmediately; дерево полей интерфейса — getConfigStructure; финальную цену отдаёт calculateConcrete вместо устаревшего Calculate из легаси.
Если дочерние профили не требуются, просто считаете тариф в родительском методе и возвращаете CalculationResult: $row = new \Bitrix\Sale\Delivery\CalculationResult(), далее setDeliveryPrice(roundEx(..., SALE_VALUE_PRECISION)) и при необходимости setPeriodDescription.
Если нужны несколько тарифных «листьев», включаете canHasProfiles, перечисляете классы профилей в getChildrenClassNames; родительский calculateConcrete тогда имеет смысл завершать SystemException, чтобы сумму не смог посчитать корень службы напрямую.
namespace Logistics\VioletRoute;
use Bitrix\Main\SystemException;
use Bitrix\Sale\Delivery\Services\Base;
use Bitrix\Sale\Delivery\Services\Manager;
use Bitrix\Sale\Delivery\CalculationResult;
use Bitrix\Sale\Shipment;
use Bitrix\Sale\Location\LocationTable;
final class HarborPostalHub extends Base
{
protected static $canHasProfiles = true;
protected static $isCalculatePriceImmediately = true;
protected static $whetherAdminExtraServicesShow = false;
public function __construct(array $payload)
{
parent::__construct($payload);
}
public static function getClassTitle(): string
{
return 'HarborPostal: складской консолидатор';
}
public static function getClassDescription(): string
{
return 'Родитель хранит токены; конкретные окна задают профили.';
}
public function isCalculatePriceImmediately()
{
return self::$isCalculatePriceImmediately;
}
public static function whetherAdminExtraServicesShow()
{
return self::$whetherAdminExtraServicesShow;
}
public static function canHasProfiles()
{
return self::$canHasProfiles;
}
public static function getChildrenClassNames()
{
return [HarborPostalRushProfile::class];
}
public function getProfilesList()
{
return ['Срочный слот'];
}
protected function getConfigStructure(): array
{
return [
'MAIN' => [
'TITLE' => 'Ключ доступа оператора',
'ITEMS' => [
'PARTNER_TICKET' => [
'TYPE' => 'STRING',
'NAME' => 'Токен оператора перевозки',
],
'DRY_ONLY' => [
'TYPE' => 'Y/N',
'NAME' => 'Разрешены только сугубо сухие грузы',
'DEFAULT' => 'N',
],
'VESSEL_SHAPE' => [
'TYPE' => 'ENUM',
'NAME' => 'Эскиз упаковки',
'DEFAULT' => 'pallet',
'OPTIONS' => [
'pallet' => 'Поддон',
'sack' => 'Мешковина',
],
],
],
],
];
}
protected function calculateConcrete(Shipment $shipment = null): CalculationResult
{
throw new SystemException('Активируйте профиль HarborPostal — сам родитель сумму не выдаёт.');
}
}
final class HarborPostalRushProfile extends Base
{
protected static $isProfile = true;
protected $portParent;
public function __construct(array $payload)
{
parent::__construct($payload);
$this->portParent = Manager::getObjectById($this->parentId);
}
public static function getClassTitle(): string
{
return 'HarborPostal: профиль Rush';
}
public static function getClassDescription(): string
{
return 'Срочное окно тем же API, что конфигурирует родитель.';
}
public function getParentService()
{
return $this->portParent;
}
public function isCalculatePriceImmediately()
{
return $this->getParentService()->isCalculatePriceImmediately();
}
public static function isProfile()
{
return self::$isProfile;
}
protected function getConfigStructure(): array
{
return [
'MAIN' => [
'TITLE' => 'Режим экспресс',
'ITEMS' => [
'ROUTE_DIGEST' => [
'TYPE' => 'STRING',
'NAME' => 'Символический ключ маршрута',
],
],
],
];
}
protected function calculateConcrete(Shipment $shipment = null): CalculationResult
{
$quote = new CalculationResult();
if ($shipment instanceof Shipment) {
$tonnageHint = $shipment->getWeight();
$orderHandle = $shipment->getCollection()->getOrder();
$binder = $orderHandle->getPropertyCollection();
$geoChip = $binder->getDeliveryLocation()->getValue();
if ($geoChip !== '' && ($line = LocationTable::getRowById($geoChip))) {
$geoChip = (string)$line['CODE'];
}
unset($tonnageHint, $orderHandle, $binder, $geoChip);
}
/* Внешний тариф держите в Data\Cache по составному ключу вес / локаль / промо */
$sharedTicket = $this->getParentService()->config['MAIN']['PARTNER_TICKET'] ?? '';
$routingDigest = $this->config['MAIN']['ROUTE_DIGEST'] ?? '';
unset($sharedTicket, $routingDigest);
$quote->setDeliveryPrice(roundEx(640.0, SALE_VALUE_PRECISION));
$quote->setPeriodDescription('2–3 рабочих дня');
return $quote;
}
public function isCompatible(Shipment $shipment)
{
$probe = $this->calculateConcrete($shipment);
return $probe->isSuccess();
}
}Через $shipment берутся getWeight(), заказ через коллекцию, свойства заказа и значение локации. В старых сценариях оформления в параметр координаты иногда утекает числовой ключ местоположения; до полного перехода на символьные коды имеет смысл подтягивать CODE строкой через LocationTable::getRowById, как в блоке выше.
Для упрощённой проверки доступности метода доставки переопределите isCompatible у профиля: вызывайте $this->calculateConcrete() и смотрите ->isSuccess(). У родителя, который завершается SystemException, отдельный isCompatible обычно не нужен — оставляют поведение ядра.
CalculationResult допускает setExtraServicesPrice, текст сроков и покупательские ошибки: $quote->addError(new \Bitrix\Main\Error('Служба недоступна для региона'));.
Точечная правка после расчёта
Событие onSaleDeliveryServiceCalculate отдаёт RESULT (CalculationResult) и SHIPMENT. Так можно добавить упаковку или сервисный сбор ко всем способам, не редактируя каждый класс.
use Bitrix\Main\EventManager;
use Bitrix\Main\EventResult;
EventManager::getInstance()->addEventHandler(
'sale',
'onSaleDeliveryServiceCalculate',
static function (\Bitrix\Main\Event $event): EventResult {
$priced = $event->getParameter('RESULT');
$priced->setDeliveryPrice($priced->getDeliveryPrice() + 185.0);
return new EventResult(EventResult::SUCCESS, ['RESULT' => $priced]);
}
);Вывод для продакшена
- Котирующие запросы к партнёрскому API всегда кешируйте на короткий TTL с ключом по весу, локации и активным промо.
- HTTP вызывайте через
\Bitrix\Main\Web\HttpClientи задавайте таймауты, чтобы cron-агент не зависал. - Ошибки интеграции превращайте в
CalculationResult::addError, чтобы витрина показывала текст, а не белый экран исключений.
Не хотите копаться сами?
Починю за 1-3 дня. Без предоплаты — оплата по результату.
15+ лет опыта с 1С-Битрикс · Без предоплаты · 7 дней гарантии