Обмен, импорт и экспорт заказов

Обмен заказами связывает объекты модуля sale с учетной, логистической или другой внешней системой. Интеграция выполняет три задачи:

  • получает заказ вместе с корзиной, свойствами, оплатами и отгрузками,

  • сопоставляет внешние и внутренние идентификаторы,

  • применяет входящие изменения через API заказа.

Модуль содержит штатный механизм обмена с 1С и классы пространства имен Bitrix\Sale\Exchange. Для интеграции с произвольной системой можно использовать объектную модель заказа и собственный клиент внешней системы. Выбор зависит от формата данных и протокола внешней системы.

Выбрать сценарий обмена

Импорт и экспорт обозначают направление данных относительно интернет-магазина.

Сценарий

Направление

Основная задача

Подходящий API

Экспорт заказа

Из интернет-магазина во внешнюю систему

Прочитать данные заказа и преобразовать их во внешний формат

Класс Bitrix\Sale\Order и связанная объектная модель. Штатный менеджер экспорта для поддерживаемого формата обмена

Импорт заказа или изменений

Из внешней системы в интернет-магазин

Найти заказ по идентификатору, проверить данные и применить изменения

Штатный менеджер импорта для поддерживаемого формата. Класс Bitrix\Sale\Order для собственной интеграции

Двусторонняя синхронизация

В обоих направлениях

Сопоставить идентификаторы, статусы и версии данных, исключить повторную обработку

Механизм обмена и журнал интеграции

Передача заказа в произвольный сервис

Из интернет-магазина во внешний HTTP API или очередь

Определить формат передаваемых данных и обработать ответ сервиса

Класс Bitrix\Sale\Order, клиент внешнего сервиса и журнал интеграции

Используйте штатный обмен с 1С для систем, которые поддерживают его формат. Для системы с другим форматом создайте собственный клиент внешнего API. Внутренние классы протокола не заменяют такой клиент.

Выбрать способ интеграции

Для обмена с 1С используйте штатные настройки и обработчики поддерживаемого протокола. Компонент bitrix:sale.export.1c применяет CSaleExport при формировании выгрузки и CSaleOrderLoader при загрузке входящих данных. Не копируйте внутреннюю реализацию обработчика в собственный модуль.

Для произвольной интеграции создайте отдельный слой:

  1. Клиент или обработчик внешней системы получает сообщение.

  2. Валидатор проверяет формат и состав входящих данных.

  3. Слой сопоставления преобразует внешние идентификаторы и состояния во внутренние.

  4. Сервис интеграции загружает и изменяет Bitrix\Sale\Order.

  5. Журнал интеграции фиксирует попытку, результат и возможность повторного запуска.

При таком разделении код интеграции не зависит от внутреннего формата штатного обмена. Получение данных, сопоставление и изменение заказа можно тестировать отдельно.

Подготовить данные и выбрать API

До запуска обмена подготовьте:

  • настройки штатного обмена или отдельные учетные данные внешнего сервиса,

  • таблицы сопоставления заказов, товаров, статусов, оплат, отгрузок и свойств,

  • свойства заказа для всех типов плательщика, с которыми работает интеграция,

  • журнал сообщений с уникальным идентификатором операции,

  • очередь повторных попыток и правила ручной обработки конфликтов,

  • технического пользователя или другой способ аутентификации входящего запроса.

Проверьте права в точке входа интеграции. Объектная модель заказа не определяет, имеет ли вызывающий пользователь право импортировать данные. Для фонового процесса ограничьте доступ к обработчику, а для операции из пользовательского запроса примените проверки прав проекта до загрузки и изменения заказа.

Определить данные заказа в обмене

Перед передачей данных определите формат и состав данных для обмена. Укажите обязательные поля, внешний идентификатор каждого объекта, формат дат и сумм, правила для пустых значений и допустимые переходы состояния.

Данные

Объект модуля sale

Что согласовать с внешней системой

Номер, дата, покупатель, статус, сумма и валюта

Bitrix\Sale\Order

Отличие внутреннего ID от номера заказа и внешнего идентификатора

Товары, количество, цена, скидка и НДС

Bitrix\Sale\Basket

Идентификатор товара или предложения, единица измерения и правило пересчета цены

Контактные данные и реквизиты

Bitrix\Sale\PropertyValueCollection

Символьный код свойства, тип значения и правило обработки отсутствующего свойства

Оплаты

Bitrix\Sale\PaymentCollection

Идентификатор платежа, платежная система, сумма и признак оплаты

Отгрузки

Bitrix\Sale\ShipmentCollection

Идентификатор отгрузки, служба доставки, состав, трек-номер и признак отгрузки

Состояние синхронизации

Данные интеграции

Внешний идентификатор, версия сообщения, дата попытки и результат обработки

Храните отдельное сопоставление внутреннего и внешнего идентификаторов в данных интеграции. Не используйте номер заказа как единственный ключ синхронизации, если внешняя система может изменить или повторно использовать номер.

Выбрать API механизма обмена

Пространство имен Bitrix\Sale\Exchange содержит инфраструктуру штатного импорта и экспорта. Перед реализацией выберите API по задаче интеграции.

API

Роль

Bitrix\Sale\Exchange\ManagerImport

Регистрирует настройки, правила разрешения коллизий и критерии поиска для типов импортируемых объектов

Bitrix\Sale\Exchange\ManagerExport

Регистрирует настройки типов экспортируемых объектов и определяет направление и режим штатного обмена

Bitrix\Sale\Exchange\Entity\EntityImport

Внутренний базовый класс для импорта заказа, оплат, отгрузок и профиля покупателя

Bitrix\Sale\Order

Загружает и изменяет заказ вместе со связанными коллекциями

Bitrix\Sale\TradingPlatform\Manager

Возвращает активные торговые платформы и объект платформы по идентификатору

Bitrix\Sale\TradingPlatform\Platform

Базовый класс обработчика торговой платформы

Классы ManagerImport и ManagerExport обслуживают внутреннюю конфигурацию штатных пакетов обмена. Метод registerInstance() обоих классов помечен как внутренний. Не собирайте собственный импорт или экспорт прямыми вызовами этих менеджеров. Используйте штатную точку обмена с 1С или объектную модель заказа для собственного протокола.

Импорт 1С записывает в объекты обмена поля ID_1C и VERSION_1C, а изменения помечает полем UPDATED_1C. У заказа также есть признак EXTERNAL_ORDER. Эти поля относятся к штатному механизму. Для произвольного протокола храните отдельную таблицу сопоставления, если внешний идентификатор не является идентификатором 1С.

Как запускается штатный обмен с 1С

Служебная точка /bitrix/admin/1c_exchange.php?type=sale подключает компонент bitrix:sale.export.1c. Компонент проверяет авторизацию и права группы, принимает файлы обмена, запускает импорт и формирует ответ протокола. Внутри компонента используются CSaleExport, CSaleOrderLoader и классы Bitrix\Sale\Exchange.

Работа компонента зависит от настроек модуля.

Настройка

Значение и роль

Значение по умолчанию

1C_SALE_SITE_LIST

Строковый идентификатор сайта ограничивает заказы, которые участвуют в обмене

Пустая строка — без фильтра по сайту

1C_EXPORT_PAYED_ORDERS

Y разрешает экспортировать только оплаченные заказы

Пустая строка — фильтр выключен

1C_EXPORT_ALLOW_DELIVERY_ORDERS

Y разрешает экспортировать только заказы с разрешенной доставкой

Пустая строка — фильтр выключен

1C_EXPORT_FINAL_ORDERS

Строковый код статуса задает начальный статус отбора. Механизм включает его и следующие статусы по сортировке

Пустая строка — фильтр выключен

1C_CHANGE_STATUS_FROM_1C

Y разрешает менять статус заказа по входящим данным 1С

Пустая строка — изменение выключено

1C_IMPORT_NEW_ORDERS

Y разрешает импортировать новые заказы, N запрещает

N

1C_SALE_GROUP_PERMISSIONS

Строка с идентификаторами групп через запятую задает доступ к точке обмена

1

Настраивайте эти параметры в интерфейсе модуля. Не вызывайте компонент или служебную точку из PHP-кода проекта для запуска произвольной интеграции.

Классы CSaleExport, CSaleOrderLoader и связанные обработчики классического API остаются частью отдельных штатных сценариев и совместимости существующих проектов. Не выбирайте эти классы как основной API для новой произвольной интеграции. Для чтения и изменения заказа используйте объектную модель Bitrix\Sale\Order, если конкретный штатный обработчик не требует использования классического API.

Реализовать экспорт и импорт

Для произвольной интеграции используйте объектную модель заказа. При экспорте соберите данные без изменения заказа. При импорте проверьте входящие значения и сохраните изменения одной операцией.

Подготовить заказ к экспорту

Экспорт должен читать сохраненное состояние заказа и не менять его. Загрузите заказ непосредственно перед формированием сообщения, соберите только согласованные поля и преобразуйте объекты в массив данных.

use Bitrix\Main\Loader;
use Bitrix\Sale\Order;

if (!Loader::includeModule('sale'))
{
    throw new \RuntimeException('Не удалось подключить модуль sale');
}

$orderId = 123;
$order = Order::load($orderId);

if (!$order)
{
    throw new \RuntimeException('Заказ не найден');
}

$exchangeData = [
    'ORDER' => [
        'ID' => $order->getId(),
        'ACCOUNT_NUMBER' => (string) $order->getField('ACCOUNT_NUMBER'),
        'STATUS_ID' => (string) $order->getField('STATUS_ID'),
        'PRICE' => $order->getPrice(),
        'CURRENCY' => $order->getCurrency(),
    ],
    'ITEMS' => [],
    'PROPERTIES' => [],
    'PAYMENTS' => [],
    'SHIPMENTS' => [],
];

foreach ($order->getBasket() as $basketItem)
{
    $exchangeData['ITEMS'][] = [
        'ID' => $basketItem->getId(),
        'PRODUCT_ID' => (int) $basketItem->getField('PRODUCT_ID'),
        'NAME' => (string) $basketItem->getField('NAME'),
        'QUANTITY' => $basketItem->getQuantity(),
        'PRICE' => $basketItem->getPrice(),
        'CURRENCY' => (string) $basketItem->getField('CURRENCY'),
    ];
}

foreach ($order->getPropertyCollection() as $propertyValue)
{
    $property = $propertyValue->getProperty();

    $exchangeData['PROPERTIES'][] = [
        'CODE' => (string) ($property['CODE'] ?? ''),
        'VALUE' => $propertyValue->getValue(),
    ];
}

foreach ($order->getPaymentCollection() as $payment)
{
    $exchangeData['PAYMENTS'][] = [
        'ID' => $payment->getId(),
        'PAY_SYSTEM_ID' => $payment->getPaymentSystemId(),
        'SUM' => $payment->getSum(),
        'PAID' => $payment->isPaid(),
    ];
}

foreach ($order->getShipmentCollection() as $shipment)
{
    if ($shipment->isSystem())
    {
        continue;
    }

    $exchangeData['SHIPMENTS'][] = [
        'ID' => $shipment->getId(),
        'DELIVERY_ID' => $shipment->getDeliveryId(),
        'PRICE' => $shipment->getPrice(),
        'SHIPPED' => $shipment->isShipped(),
        'TRACKING_NUMBER' => (string) $shipment->getField(
            'TRACKING_NUMBER'
        ),
    ];
}

Массив $exchangeData не является форматом штатного обмена с 1С. Это пример формата данных для произвольной интеграции. Передайте массив в сериализатор, HTTP-клиент или очередь проекта и отдельно обработайте результат отправки.

Не вызывайте Order::save() в коде экспорта. Если нужно отметить успешную передачу, сохраните состояние синхронизации в отдельном журнале интеграции или измените заказ в отдельной операции с явной проверкой результата.

Обработать входящее изменение заказа

Разделите импорт на этапы: проверку сообщения, поиск заказа, сопоставление значений, изменение объектов и фиксацию результата. Не передавайте внешние данные напрямую в поля заказа.

  1. Проверьте источник сообщения и право интеграции выполнять операцию.

  2. Проверьте идентификатор сообщения, формат и обязательные поля.

  3. Найдите сопоставленный заказ и убедитесь, что сообщение еще не обработано.

  4. Сопоставьте с внутренними значениями внешние статусы, идентификаторы оплат и отгрузок, а также значения свойств.

  5. Измените загруженный объект заказа и связанные коллекции.

  6. Сохраните заказ через Order::save() и обработайте ошибки.

  7. Только после успешного сохранения отметьте сообщение обработанным.

Этот сценарий обновляет существующий заказ. Если внешняя система должна создавать новые заказы, сначала согласуйте обязательные настройки корзины, сайта, покупателя и типа плательщика, а также оплат и отгрузок, если импорт создает эти объекты. После сохранения нового заказа запишите соответствие его внутреннего и внешнего идентификаторов. Полный порядок создания заказа — в статье Создание заказа.

В примере используются уже проверенные внутренний код статуса и значение свойства. Поиск свойства по коду не создает новое свойство заказа.

use Bitrix\Main\Loader;
use Bitrix\Sale\Order;

if (!Loader::includeModule('sale'))
{
    throw new \RuntimeException('Не удалось подключить модуль sale');
}

// Внутренний идентификатор заказа из таблицы сопоставления
$orderId = 123;
// Внутренний код статуса после сопоставления
$statusId = 'P';
// Значение свойства после проверки формата
$externalComment = 'Изменено учетной системой';

$order = Order::load($orderId);

if (!$order)
{
    throw new \RuntimeException('Заказ не найден');
}

$statusResult = $order->setField('STATUS_ID', $statusId);

if (!$statusResult->isSuccess())
{
    throw new \RuntimeException(
        implode('; ', $statusResult->getErrorMessages())
    );
}

$property = $order
    ->getPropertyCollection()
    ->getItemByOrderPropertyCode('EXTERNAL_COMMENT')
;

if (!$property)
{
    throw new \RuntimeException(
        'Свойство заказа EXTERNAL_COMMENT не найдено'
    );
}

$propertyResult = $property->setValue($externalComment);

if (!$propertyResult->isSuccess())
{
    throw new \RuntimeException(
        implode('; ', $propertyResult->getErrorMessages())
    );
}

$saveResult = $order->save();

if (!$saveResult->isSuccess())
{
    throw new \RuntimeException(
        implode('; ', $saveResult->getErrorMessages())
    );
}

Код статуса P и свойство EXTERNAL_COMMENT условные. Создайте их в настройках проекта или замените фактическими значениями. Если входящие данные меняют корзину, стоимость доставки, скидки или налоги, пересчитайте заказ до сохранения. Порядок изменений и пересчета описан в статье Изменение и чтение заказа.

Сопоставить состояния и связанные объекты

Внешний статус не должен автоматически становиться кодом статуса заказа. Храните явные таблицы сопоставления и отклоняйте неизвестные значения.

Внешние данные

Внутренний объект

Правило сопоставления

Состояние заказа

Order::STATUS_ID

Разрешайте только переходы, которые поддерживает процесс магазина

Состояние оплаты

Объект Payment

Найдите конкретную оплату по сохраненному соответствию и изменяйте ее через объектную модель

Состояние доставки

Объект Shipment

Не смешивайте статус отгрузки, разрешение доставки и трек-номер

Товар

Позиция BasketItem

Сопоставляйте внешний код с товаром или торговым предложением и проверяйте единицу измерения

Реквизит покупателя

Значение свойства заказа

Сопоставляйте по стабильному символьному коду, а не по отображаемому названию

Одна внешняя операция может изменить несколько частей заказа. Применяйте связанные изменения к одному загруженному объекту и вызывайте Order::save() один раз после проверок и необходимых расчетов.

Не подтверждайте оплату только по общему статусу внешнего заказа. Для финансовой операции нужны идентификатор конкретного платежа, сумма, валюта и подтвержденный ответ платежной или учетной системы.

Сохранить свойства заказа при импорте

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

Для каждого входящего свойства:

  1. Найдите внутреннее свойство по заранее настроенному символьному коду.

  2. Проверьте тип и допустимый формат значения.

  3. Измените найденный объект значения свойства.

  4. Оставьте остальные свойства без изменений.

Если свойство не найдено, не создавайте его автоматически в процессе импорта. Зафиксируйте ошибку сопоставления и настройте свойство для нужного типа плательщика.

Как обеспечить надежность обмена

Надежный обмен должен корректно обрабатывать повторные сообщения, одновременные изменения и ошибки. Настройте повторный запуск, разрешение конфликтов и журналирование.

Обеспечить повторный запуск

Внешняя система может повторно отправить сообщение после тайм-аута или из-за того, что не получила ответ интернет-магазина. Обрабатывайте повторное сообщение без повторного изменения оплаты, количества товара или статуса.

Сохраните в журнале интеграции:

  • идентификатор источника и сообщения,

  • внутренний и внешний идентификаторы заказа,

  • тип операции и версию данных,

  • время получения и обработки,

  • результат и текст ошибки,

  • контрольную сумму значимых данных, если внешний протокол не передает версию.

Перед изменением заказа проверьте уникальность сообщения. Записывайте успешный результат после Order::save(). Если внешний протокол поддерживает подтверждение, отправляйте его после фиксации результата.

Обработать конкурирующие изменения

Импорт может выполняться одновременно с редактированием заказа менеджером или другим интеграционным процессом. До изменения сравните версию внешних данных с последним обработанным состоянием. Заранее определите владельца каждого поля. Укажите, какая система может менять значение и чьи данные имеют приоритет при конфликте.

Сначала выполните HTTP-запрос к внешней системе и проверьте сообщение. Затем измените заказ локально. Если изменение выполняется в транзакции базы данных, завершите ее сразу после попытки сохранить заказ. При успешном сохранении зафиксируйте изменения, при ошибке — откатите. Для очереди повторно загрузите заказ непосредственно перед изменением.

Если конфликт нельзя разрешить автоматически, не перезаписывайте локальные данные. Зафиксируйте конфликт в журнале и передайте его на ручную обработку.

Изменение и сохранение заказа может запускать стандартные события и другие обработчики проекта. Проверяйте источник операции перед обратным экспортом, иначе входящий импорт может запустить рекурсивный обмен. Сохраняйте признак источника вместе с данными операции. Обработчик экспорта должен игнорировать изменения, которые внесла эта же интеграция.

Обработать ошибки обмена

Разделяйте ошибки по этапам. Это помогает решить, можно ли повторить операцию.

Этап

Пример ошибки

Действие

Связь

Тайм-аут или недоступность внешнего сервиса

Сохранить попытку и повторить операцию ограниченное число раз

Формат

Нет обязательного идентификатора или неверный тип значения

Отклонить сообщение без изменения заказа

Сопоставление

Неизвестен код статуса, товара или свойства

Остановить обработку и исправить настройки сопоставления

Бизнес-правило

Недопустимый переход статуса или изменение оплаченной суммы

Отклонить изменение и передать конфликт на ручную обработку

Сохранение

Order::save() вернул ошибки

Не отмечать сообщение обработанным. Записать сообщения из Result

Не записывайте в журнал токены доступа, пароли, платежные реквизиты и полные персональные данные. Для диагностики сохраняйте технические идентификаторы, этап, код ошибки и безопасный фрагмент ответа.

Пример записи ошибки через стандартный журнал проекта:

$orderId = 123;

try
{
    // Получите, проверьте и примените сообщение обмена
}
catch (\Throwable $exception)
{
    AddMessage2Log(
        sprintf(
            'Ошибка импорта заказа %d: %s',
            $orderId,
            $exception->getMessage()
        ),
        'sale_exchange'
    );

    throw $exception;
}

Функция AddMessage2Log() подходит для технической диагностики, но не заменяет журнал интеграции для контроля повторных сообщений. В журнале должен быть машинно-читаемый статус операции и уникальный идентификатор сообщения.