Ограничения служб доставки в Битрикс D7: расширенные возможности настройки

Введение

При переходе интернет-магазина на платформу Bitrix D7 в модуле доставки реализована система гибких ограничений, позволяющая задавать условия доступности служб доставки на основе:

  • Географического местоположения
  • Суммарной стоимости заказа
  • Веса товаров
  • Пользовательских параметров
  • и др.

Расширение функциональности ограничений

Механизм подключения пользовательских ограничений

Для интеграции кастомных ограничений используйте системное событие onSaleDeliveryRestrictionsClassNamesBuildList:

$eventManager = \Bitrix\Main\EventManager::getInstance();
$eventManager->addEventHandler(
    'sale',
    'onSaleDeliveryRestrictionsClassNamesBuildList',
    'registerCustomRestrictions'
);

function registerCustomRestrictions() {
    return new \Bitrix\Main\EventResult(
        \Bitrix\Main\EventResult::SUCCESS,
        [
            '\Vendor\Module\DeliveryRestrictions\CustomRestriction' => '/local/modules/vendor.module/lib/restriction.php'
        ]
    );
}

Архитектура классов ограничений

Базовые требования

Класс ограничения должен наследоваться от абстрактного класса Bitrix\Sale\Delivery\Restrictions\Base. Рекомендуется использовать существующие реализации в директории /bitrix/modules/sale/lib/delivery/restrictions/ как базовые.

Минимальная реализация класса:

namespace Vendor\Module\DeliveryRestrictions;

class CustomRestriction extends \Bitrix\Sale\Delivery\Restrictions\Base 
{
    // Название ограничения
    public static function getClassTitle(): string 
    {
        return 'Пользовательское ограничение';
    }

    // Описание ограничения
    public static function getClassDescription(): string 
    {
        return 'Ограничение доставки по кастомным параметрам';
    }

    protected static function extractParams(\Bitrix\Sale\Shipment $shipment): array 
    {
        return [];
    }

    public static function getParamsStructure(int $deliveryId = 0): array 
    {
        return [];
    }

    public static function check($shipmentParams, array $restrictionParams, int $deliveryId = 0): bool 
    {
        return true;
    }
}

Ключевые методы

  1. extractParams()\ Извлекает параметры отгрузки для последующей проверки:

    protected static function extractParams(\Bitrix\Sale\Shipment $shipment): array 
    {
       $params = [];
       $order = $shipment->getCollection()->getOrder();
    
       // Получение свойств заказа
       if ($propCollection = $order->getPropertyCollection()) {
           $params['LOCATION'] = $propCollection->getDeliveryLocation()->getValue();
       }
    
       return $params;
    }
  2. check()\ Выполняет бизнес-логику проверки ограничения:

    public static function check($shipmentParams, array $restrictionParams, int $deliveryId = 0): bool 
    {
       return $restrictionParams['MAX_WEIGHT'] >= $shipmentParams['TOTAL_WEIGHT'];
    }
  3. getParamsStructure()\ Определяет структуру параметров для интерфейса администратора:

    public static function getParamsStructure(int $deliveryId = 0): array 
    {
       return [
           'MAX_WEIGHT' => [
               'TYPE' => 'NUMBER',
               'LABEL' => 'Максимальный вес (кг)',
               'MIN' => 0,
               'DEFAULT' => 50
           ]
       ];
    }

Расширенные возможности

Приоритет выполнения ограничений

Используйте статическое свойство $easeSort для управления порядком проверки:

public static int $easeSort = 200;
  • 100: Быстрые проверки (рекомендуется по умолчанию)
  • 200: Проверки с обращениями к БД
  • 300: Ресурсоемкие вычисления

Кастомное хранение параметров

Переопределите методы работы с персистентным слоем:

public static function save(array $fields, int $restrictionId = 0): \Bitrix\Main\Result 
{
    // Кастомная логика сохранения
    return parent::save($fields, $restrictionId);
}

public static function delete(int $restrictionId, int $deliveryId = 0): \Bitrix\Main\Result 
{
    // Кастомная логика удаления
    return parent::delete($restrictionId);
}

Пример реализации: ограничение по исключающим локациям

class ExcludeLocationRestriction extends \Bitrix\Sale\Delivery\Restrictions\Base 
{
    public static $easeSort = 200;

    public static function check($locationCode, array $restrictionParams, int $deliveryId = 0): bool 
    {
        return !\Bitrix\Sale\Delivery\DeliveryLocationTable::checkConnectionExists(
            $deliveryId,
            $locationCode,
            ['LOCATION_LINK_TYPE' => 'AUTO']
        );
    }

    protected static function extractParams(\Bitrix\Sale\Shipment $shipment): string 
    {
        return $shipment->getCollection()->getOrder()
            ->getPropertyCollection()
            ->getDeliveryLocation()
            ->getValue() ?? '';
    }
}

Еще примеры

Метод extractParams принимает объект отгрузки Bitrix\Sale\Shipment и должен подготовить необходимые данные для проверки ограничения и вернуть их.

Далее эти данные передаются в метод check. В самом простом случае можно вернуть null.

\$shipment - объект отгрузки, для которой идёт подбор подходящих служб доставки. Примеры того, что можно получить из этого объекта:

protected static function extractParams(Bitrix\Sale\Shipment $shipment)
{
    $someShipmentParams = array();

    // Получаем товары в корзине:
    foreach ($shipment->getShipmentItemCollection() as $shipmentItem) {
        /** @var \Bitrix\Sale\BasketItem $basketItem - запись в корзине*/
        $basketItem = $shipmentItem->getBasketItem();
        // ...
    }

    // Получаем информацию о заказе:
    /** @var \Bitrix\Sale\ShipmentCollection $collection - коллекция всех отгрузок в заказе */
    $collection = $shipment->getCollection();
    /** @var \Bitrix\Sale\Order $order - объект заказа*/
    $order = $collection->getOrder();

    // Получаем выбранные оплаты:
    /** @var \Bitrix\Sale\Payment $payment - объект оплаты */
    foreach($order->getPaymentCollection() as $payment) {
        /** @var int $paySystemId - ID способа оплаты*/
        $paySystemId = $payment->getPaymentSystemId();
        // ...
        $someShipmentParams["paySystem"] = $paySystemId;
    }
    //...

    return $someShipmentParams;
}

Если вы планируете значения параметров в своей таблице (сомнительная возможность, но может пригодиться), то можно переопределить следующие методы:

// Получение значений параметров
public static function prepareParamsValues(array $paramsValues, $deliveryId = 0)
{
    // Достаём откуда-то значения
    $paramsValues = array("MY_PARAM" => $value);

    return $paramsValues;
}
// Сохранение ограничения
public static function save(array $fields, $restrictionId = 0)
{
    $params = $fields["PARAMS"];
    $fields["PARAMS"] = array();

    $result = parent::save($fields, $restrictionId);
    // Сохраняем куда надо
    self::saveToYourPlace($fields["SERVICE_ID"], $params); // Здесь может быть ваш метод или код

    return $result;
}
// Удаление ограничения
public static function delete($restrictionId, $deliveryId = 0)
{
    self::deleteFromYourPlace($fields["SERVICE_ID"]); // Здесь может быть ваш метод или код

    return parent::delete($restrictionId);
}

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

В новых ограничениях с целью совместимости данная таблица еще существует и новое ограничение реализовано переопределением prepareParamsValues, save и delete.


Ограничение "Все местоположения, кроме"

Это немного изменённое стандартное ограничение по местоположению:

// /bitrix/php_interface/init.php:
$eventManager = \Bitrix\Main\EventManager::getInstance();
$eventManager->addEventHandler(
    'sale',
    'onSaleDeliveryRestrictionsClassNamesBuildList',
    'myDeliveryRestrictions'
);

function myDeliveryRestrictions()
{
    return new \Bitrix\Main\EventResult(
        \Bitrix\Main\EventResult::SUCCESS,
        array(
            '\ExclLocationsDeliveryRestriction' => '/bitrix/php_interface/include/ExclLocationsDeliveryRestriction.php',
        )
    );
}

// /bitrix/php_interface/include/ExclLocationsDeliveryRestriction.php:

use Bitrix\Sale\Delivery\DeliveryLocationTable,
    Bitrix\Sale\Internals\CollectableEntity,
    Bitrix\Sale\Shipment;

class ExclLocationsDeliveryRestriction extends \Bitrix\Sale\Delivery\Restrictions\Base
{
    public static $easeSort = 200;

    public static function getClassTitle()
    {
        return 'По местоположению (Все, кроме)';
    }

    public static function getClassDescription()
    {
        return 'По местоположению (Все, кроме)';
    }

    public static function check($locationCode, array $restrictionParams, $deliveryId = 0)
    {
        if (intval($deliveryId) <= 0) {
            return true;
        }

        if (strlen($locationCode) <= 0) {
            return false;
        }

        try {
            return !DeliveryLocationTable::checkConnectionExists(
                intval($deliveryId),
                $locationCode,
                array(
                    'LOCATION_LINK_TYPE' => 'AUTO'
                )
            );
        }
        catch(\Bitrix\Sale\Location\Tree\NodeNotFoundException $e) {
            return false;
        }
    }

    protected static function extractParams(CollectableEntity $shipment)
    {
        $order = $shipment->getCollection()->getOrder();

        if (!$props = $order->getPropertyCollection()) {
            return '';
        }

        if (!$locationProp = $props->getDeliveryLocation()) {
            return '';
        }

        if (!$locationCode = $locationProp->getValue()) {
            return '';
        }

        return $locationCode;
    }

    protected static function prepareParamsForSaving(array $params = array(), $deliveryId = 0)
    {
        if ($deliveryId > 0) {
            $arLocation = array();

            if (!!\CSaleLocation::isLocationProEnabled()) {
                if (strlen($params["LOCATION"]['L'])) {
                    $LOCATION1 = explode(':', $params["LOCATION"]['L']);
                }

                if (strlen($params["LOCATION"]['G'])) {
                    $LOCATION2 = explode(':', $params["LOCATION"]['G']);
                }
            }

            if (isset($LOCATION1) && is_array($LOCATION1) && count($LOCATION1) > 0) {
                $arLocation["L"] = array();
                $locationCount = count($LOCATION1);

                for ($i = 0; $i < $locationCount; $i++) {
                    if (strlen($LOCATION1[$i])) {
                        $arLocation["L"][] = $LOCATION1[$i];
                    }
                }
            }

            if (isset($LOCATION2) && is_array($LOCATION2) && count($LOCATION2) > 0) {
                $arLocation["G"] = array();
                $locationCount = count($LOCATION2);

                for ($i = 0; $i < $locationCount; $i++) {
                    if (strlen($LOCATION2[$i])) {
                        $arLocation["G"][] = $LOCATION2[$i];
                    }
                }

            }

            DeliveryLocationTable::resetMultipleForOwner($deliveryId, $arLocation);
        }

        return array();
    }

    public static function getParamsStructure($deliveryId = 0)
    {

        $result =  array(
            "LOCATION" => array(
                "TYPE" => "LOCATION_MULTI"
            ),
        );

        if ($deliveryId > 0) {
            $result["LOCATION"]["DELIVERY_ID"] = $deliveryId;
        }

        return $result;
    }

    public static function save(array $fields, $restrictionId = 0)
    {
        $fields["PARAMS"] = self::prepareParamsForSaving($fields["PARAMS"], $fields["SERVICE_ID"]);

        return parent::save($fields, $restrictionId);
    }

    public static function delete($restrictionId, $deliveryId = 0)
    {
        DeliveryLocationTable::resetMultipleForOwner($deliveryId);

        return parent::delete($restrictionId);
    }
}

Решение типовых ошибок

Ошибка совместимости типов

Declaration of CustomRestriction::extractParams(CollectableEntity $shipment) 
must be compatible with Base::extractParams(Entity $entity)

Решение: Приведите тип параметра к базовому классу:

protected static function extractParams(\Bitrix\Sale\Internals\Entity $entity): array 
{
    if (!$entity instanceof \Bitrix\Sale\Shipment) {
        return [];
    }

    // Логика обработки Shipment
}

Заключение

Представленная архитектура позволяет реализовывать сложные сценарии ограничений доставки с полной интеграцией в ядро Bitrix D7. Для оптимальной производительности рекомендуется:

  • Минимизировать обращения к БД в методах проверки
  • Использовать кеширование повторяющихся вычислений
  • Соблюдать принцип инкапсуляции при работе с объектами заказа