Перейти к содержанию

Обмен заказами связывает объекты модуля 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() подходит для технической диагностики, но не заменяет журнал интеграции для контроля повторных сообщений. В журнале должен быть машинно-читаемый статус операции и уникальный идентификатор сообщения.