Двусторонняя синхронизация статусов: CMS ↔ CRM в Bitrix
Если в твоём проекте на Bitrix:
- заказы создаются в CMS,
- сделки ведутся в CRM,
- и они должны быть синхронизированы в обе стороны,
— эта статья для тебя.
Разберём, как сделать синхронизацию статусов без циклов, без костылей и с приоритетом для оплаты.
Задача
Заказчик поставил простое требование:
Статусы заказа в CMS и статусы сделки в CRM должны быть синхронизированы в обе стороны.
На практике это означает:
- изменение статуса заказа в CMS → сразу обновляет сделку в CRM
- изменение статуса сделки в CRM → сразу обновляет заказ в CMS
- оплата заказа должна практически мгновенно отражаться в CRM
- отсутствие зацикливаний и повторных обновлений
- стабильную работу без кронов и костылей
Почему нельзя делать «в лоб»
Если просто повесить обработчики событий без контроля источника изменений:
- CMS меняет статус → CRM ловит событие и обновляет сделку
- CRM меняет статус → CMS ловит событие и обновляет заказ
- 🔁 Начинается бесконечный цикл
- Растёт нагрузка на базу
- Статусы начинают «скакать»
- Бизнес получает неконсистентные данные
Проблема: каждый обработчик не знает, откуда пришло изменение — от пользователя или от другого обработчика.
Решение: чётко разделить сценарии и контролировать источник изменений.
Где размещать код и как подключать
Bitrix коробка: CMS + CRM на одном ядре
В коробочной версии Bitrix часто используется схема:
- CMS (сайт) — /ext_www//public_html
- CRM-портал — /ext_www/crm./
- сервер и ядро общие, но 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"]);
}
Что делает: при сохранении заказа в CMS проверяет, нужно ли обновить связанную сделку в CRM.
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/SyncCrmDealToOrder.php";
$path\_order\_to\_deal = $\_SERVER["DOCUMENT\_ROOT"] . "/local/php\_interface/lib/SyncCrmOrderToDeal.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"]);
Что делает: при изменении сделки в CRM обновляет связанный заказ в CMS.
Важный нюанс одного ядра
Несмотря на общий сервер и ядро Bitrix:
- DOCUMENT_ROOT у CMS и CRM разный
- нельзя подключать файлы «через ../» между порталами
- каждый портал работает в своём контексте событий
Такой подход:
- исключает конфликты
- не ломается при обновлениях
- предсказуем в поддержке
Архитектура решения
Синхронизация разделена на три независимых сценария:
- CMS → CRM — изменение статуса заказа
- CRM → CMS — изменение стадии сделки
- Оплата — приоритетный, мгновенный кейс
Каждый сценарий:
- реагирует только на своё событие
- проверяет текущее состояние
- не инициирует обратный вызов повторно
Сценарий 1: CMS → CRM
Триггер: изменение заказа в CMS.
Логика:
- Проверка, что статус реально изменился
- Поиск связанной сделки (через PROPERTY_ORDER_ID или другую связь)
- Маппинг статусов 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
Триггер: изменение стадии сделки в CRM.
Логика:
- Поиск связанного заказа
- Маппинг стадии сделки → статус заказа
- Обновление заказа только при расхождении
Псевдокод:
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 заказ и сделка связаны через:
- Свойство сделки PROPERTY_ORDER_ID — хранит ID заказа
- Свойство заказа PROPERTY_DEAL_ID — хранит ID сделки (опционально)
Пример получения связанной сделки:
function getLinkedDeal($orderId) {
$deal = \CCrmDeal::GetList(
[],
['UF\_ORDER\_ID' => $orderId], // или другое поле связи
false,
false,
['ID', 'STAGE\_ID']
)->Fetch();
return $deal ? $deal['ID'] : null;
}
Пример получения связанного заказа:
function getLinkedOrder($dealId) {
$deal = \CCrmDeal::GetByID($dealId);
$orderId = $deal['UF\_ORDER\_ID']; // или другое поле связи
return $orderId ?: null;
}
Маппинг статусов
Статусы заказов и стадии сделок — это разные сущности. Нужен маппинг.
Пример маппинга:
function mapOrderStatusToDeal($orderStatus) {
$mapping = [
'N' => 'NEW', // Новый → Новая
'P' => 'PREPARATION', // Принят → Подготовка
'F' => 'WON', // Выполнен → Успешно реализована
'C' => 'LOSE', // Отменён → Проиграна
];
return $mapping[$orderStatus] ?? 'NEW';
}
function mapDealStageToOrder($dealStage) {
$mapping = [
'NEW' => 'N',
'PREPARATION' => 'P',
'WON' => 'F',
'LOSE' => 'C',
];
return $mapping[$dealStage] ?? 'N';
}
Важно: маппинг должен быть симметричным и покрывать все статусы, которые используются в проекте.
Результат
После правильной настройки:
- ✅ Статусы всегда консистентны между CMS и CRM
- ✅ Оплата отображается почти мгновенно
- ✅ Нет циклов и лишних обновлений
- ✅ Нет лишних запросов к базе
- ✅ Бизнесу понятно, что происходит
Вывод
Двусторонняя синхронизация — это не «повесить обработчик и забыть».
Это:
- контроль источника изменений — знаем, откуда пришло изменение
- разделение сценариев — каждый сценарий работает независимо
- аккуратная работа с событиями Bitrix — используем правильные хуки
- проверка перед обновлением — обновляем только при расхождении
Если сделать правильно — система работает годами и не требует внимания.
Репозиторий
Исходный код с обезличенной реализацией, примерами и структурой проекта:
👉 https://github.com/va-proger/vp\_bitrix\_deal\_to\_order\_status\_sync
Top comments (0)