Двусторонняя синхронизация статусов CMS ↔ CRM в Bitrix
Практическая реализация синхронизации статусов заказов и сделок в коробочном Bitrix: CMS ↔ CRM, без циклов, с приоритетной обработкой оплаты.
🔄 Двусторонняя синхронизация статусов: 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 разный- нельзя подключать файлы «через ../» между порталами
- каждый портал работает в своём контексте событий
Такой подход:
- исключает конфликты
- не ломается при обновлениях
- предсказуем в поддержке
Архитектура решения
Синхронизация разделена на три независимых сценария:
- CMS → CRM — изменение статуса заказа
- CRM → CMS — изменение стадии сделки
- Оплата — приоритетный, мгновенный кейс
Каждый сценарий:
- реагирует только на своё событие
- проверяет текущее состояние
- не инициирует обратный вызов повторно
Сценарий 1: CMS → CRM
Триггер: изменение заказа в CMS.
Логика:
- Проверка, что статус реально изменился
- Поиск связанной сделки
- Маппинг статусов CMS → CRM
- Обновление сделки только при расхождении
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



Комментарии