Yandex Metrika
sanches.free 17 просмотров

Обработчик доставки для 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 дней гарантии