← Назад в блог

Двусторонняя синхронизация статусов CMS ↔ CRM в Bitrix

Практическая реализация синхронизации статусов заказов и сделок в коробочном Bitrix: CMS ↔ CRM, без циклов, с приоритетной обработкой оплаты.

Двусторонняя синхронизация статусов CMS ↔ CRM в Bitrix

🔄 Двусторонняя синхронизация статусов: CMS ↔ CRM (Bitrix, коробка)

Задача

Заказчик поставил, на первый взгляд, простое требование:

Статусы заказа в CMS и статусы сделки в CRM должны быть синхронизированы в обе стороны.

На практике это означает:

  • изменение статуса заказа в CMS → сразу обновляет сделку в CRM
  • изменение статуса сделки в CRM → сразу обновляет заказ в CMS
  • оплата заказа должна практически мгновенно отражаться в CRM
  • отсутствие зацикливаний и повторных обновлений
  • стабильную работу без кронов и костылей

Почему нельзя делать «в лоб»

Если не контролировать источник изменений:

  • CMS меняет статус → CRM ловит событие
  • CRM меняет статус → CMS ловит событие
  • 🔁 начинается бесконечный цикл
  • растёт нагрузка
  • статусы начинают «скакать»
  • бизнес получает неконсистентные данные

Поэтому ключевая задача — чётко разделить сценарии и точки входа.


Где размещать код и как подключать

(Bitrix коробка: CMS + CRM на одном ядре)

В коробочной версии Bitrix часто используется схема:

  • CMS (сайт)/ext_www/<domain>/public_html
  • CRM-портал/ext_www/crm.<domain>/
  • сервер и ядро общие, но DOCUMENT_ROOT разный

Поэтому код синхронизации размещается отдельно:

  • в CMS — для обработки заказов
  • в CRM — для обработки сделок

Структура одинаковая: local/php_interface/lib/ + подключение через init.php.


CMS (сайт)

Файл синхронизации:


/home/bitrix/ext_www/<domain>/public_html/local/php_interface/lib/
└── SyncCmsOrderToDeal.php

Подключение в init.php:

$path_order_to_deal = $_SERVER["DOCUMENT_ROOT"] . "/local/php_interface/lib/SyncCmsOrderToDeal.php";

if (file_exists($path_order_to_deal)) {
    require_once $path_order_to_deal;

    // Основной триггер — создание и сохранение заказа
    \Bitrix\Main\EventManager::getInstance()->addEventHandler(
        'sale',
        'OnSaleOrderSaved',
        ['SyncOrderToDeal', 'onOrderSaved']
    );

    // При необходимости — отдельный хук на смену статуса
    // AddEventHandler("sale", "OnSaleStatusOrder", ["SyncOrderToDeal", "onOrderStatusChange"]);
}

CRM (портал)

Файлы синхронизации:

/home/bitrix/ext_www/crm.<domain>/local/php_interface/lib/
├── SyncCrmDealToOrder.php
└── SyncCrmOrderToDeal.php

Подключение в init.php:

$sync_deal_to_order = $_SERVER["DOCUMENT_ROOT"] . "/local/php_interface/lib/SyncDealToOrder.php";
$path_order_to_deal = $_SERVER["DOCUMENT_ROOT"] . "/local/php_interface/lib/SyncOrderToDeal.php";

if (file_exists($sync_deal_to_order)) {
    require_once $sync_deal_to_order;
}

if (file_exists($path_order_to_deal)) {
    require_once $path_order_to_deal;
}

// CRM → CMS (изменение стадии сделки)
AddEventHandler("crm", "OnAfterCrmDealUpdate", ["SyncDealToOrder", "onDealUpdate"]);
// AddEventHandler("crm", "OnAfterCrmDealAdd", ["SyncDealToOrder", "onDealAdd"]);

// CMS → CRM (изменение статуса заказа)
AddEventHandler("sale", "OnSaleStatusOrder", ["SyncOrderToDeal", "onOrderStatusChange"]);

Важный нюанс одного ядра

Несмотря на общий сервер и ядро Bitrix:

  • DOCUMENT_ROOT у CMS и CRM разный
  • нельзя подключать файлы «через ../» между порталами
  • каждый портал работает в своём контексте событий

Такой подход:

  • исключает конфликты
  • не ломается при обновлениях
  • предсказуем в поддержке

Архитектура решения

Синхронизация разделена на три независимых сценария:

  1. CMS → CRM — изменение статуса заказа
  2. CRM → CMS — изменение стадии сделки
  3. Оплата — приоритетный, мгновенный кейс

Каждый сценарий:

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

Сценарий 1: CMS → CRM

Триггер: изменение заказа в CMS.

Логика:

  1. Проверка, что статус реально изменился
  2. Поиск связанной сделки
  3. Маппинг статусов CMS → CRM
  4. Обновление сделки только при расхождении
onOrderStatusChange(order):
    if statusNotChanged():
        return

    dealId = getLinkedDeal(order)
    if not dealId:
        return

    crmStatus = mapOrderStatusToDeal(order.status)

    if deal.status != crmStatus:
        updateDealStatus(dealId, crmStatus)

Сценарий 2: CRM → CMS

Триггер: изменение стадии сделки.

onDealStageChange(deal):
    orderId = getLinkedOrder(deal)
    if not orderId:
        return

    orderStatus = mapDealStageToOrder(deal.stage)

    if order.status != orderStatus:
        updateOrderStatus(orderId, orderStatus)

Сценарий 3: Оплата (приоритет)

Оплата — критическое событие, поэтому обновление идёт сразу:

onOrderPaid(order):
    dealId = getLinkedDeal(order)

    if deal.status != "PAID":
        updateDealStatus(dealId, "PAID")

Задержка — доли секунды.


Результат

  • ✅ Статусы всегда консистентны
  • ✅ Оплата отображается почти мгновенно
  • ✅ Нет циклов
  • ✅ Нет лишних запросов
  • ✅ Бизнесу понятно, что происходит

Вывод

Двусторонняя синхронизация — это не «повесить обработчик».

Это:

  • контроль источника изменений
  • разделение сценариев
  • аккуратная работа с событиями Bitrix

Если сделать правильно — система работает годами и не требует внимания.


🔗 Репозиторий

Исходный код с обезличенной реализацией, примерами и структурой проекта:
👉 https://github.com/va-proger/vp_bitrix_deal_to_order_status_sync

0 просмотров

Комментарии

Загрузка комментариев...