DEV Community

Андрей Викулов (VProger)
Андрей Викулов (VProger)

Posted on • Originally published at viku-lov.ru on

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

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

Если в твоём проекте на Bitrix:

  • заказы создаются в CMS,
  • сделки ведутся в CRM,
  • и они должны быть синхронизированы в обе стороны,

— эта статья для тебя.

Разберём, как сделать синхронизацию статусов без циклов, без костылей и с приоритетом для оплаты.


Задача

Заказчик поставил простое требование:

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

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

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

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

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

  1. CMS меняет статус → CRM ловит событие и обновляет сделку
  2. CRM меняет статус → CMS ловит событие и обновляет заказ
  3. 🔁 Начинается бесконечный цикл
  4. Растёт нагрузка на базу
  5. Статусы начинают «скакать»
  6. Бизнес получает неконсистентные данные

Проблема: каждый обработчик не знает, откуда пришло изменение — от пользователя или от другого обработчика.

Решение: чётко разделить сценарии и контролировать источник изменений.


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

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

Enter fullscreen mode Exit fullscreen mode

Подключение в 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"]);

}

Enter fullscreen mode Exit fullscreen mode

Что делает: при сохранении заказа в CMS проверяет, нужно ли обновить связанную сделку в CRM.


CRM (портал)

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


/home/bitrix/ext\_www/crm.<domain>/local/php\_interface/lib/

├── SyncCrmDealToOrder.php

└── SyncCrmOrderToDeal.php

Enter fullscreen mode Exit fullscreen mode

Подключение в 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"]);

Enter fullscreen mode Exit fullscreen mode

Что делает: при изменении сделки в CRM обновляет связанный заказ в CMS.


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

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

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

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

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

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

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

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

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

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

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

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

Логика:

  1. Проверка, что статус реально изменился
  2. Поиск связанной сделки (через PROPERTY_ORDER_ID или другую связь)
  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)

Enter fullscreen mode Exit fullscreen mode

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


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

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

Логика:

  1. Поиск связанного заказа
  2. Маппинг стадии сделки → статус заказа
  3. Обновление заказа только при расхождении

Псевдокод:


onDealStageChange(deal):

// Ищем связанный заказ

orderId = getLinkedOrder(deal)

if not orderId:

return

// Маппим стадию сделки на статус заказа

orderStatus = mapDealStageToOrder(deal.stage)

// Обновляем только если статусы не совпадают

if order.status != orderStatus:

updateOrderStatus(orderId, orderStatus)

Enter fullscreen mode Exit fullscreen mode

Важно: используем тот же принцип проверки — обновляем только при расхождении.


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

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

Триггер: событие оплаты заказа.

Логика:

  1. Находим связанную сделку
  2. Обновляем статус сделки на «Оплачено» немедленно

Псевдокод:


onOrderPaid(order):

dealId = getLinkedDeal(order)

// Оплата — приоритет, обновляем сразу

if deal.status != "PAID":

updateDealStatus(dealId, "PAID")

Enter fullscreen mode Exit fullscreen mode

Задержка: доли секунды. Бизнес видит оплату практически мгновенно.


Как связать заказ и сделку

В Bitrix заказ и сделка связаны через:

  1. Свойство сделки PROPERTY_ORDER_ID — хранит ID заказа
  2. Свойство заказа 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;

}

Enter fullscreen mode Exit fullscreen mode

Пример получения связанного заказа:


function getLinkedOrder($dealId) {

$deal = \CCrmDeal::GetByID($dealId);

$orderId = $deal['UF\_ORDER\_ID']; // или другое поле связи

return $orderId ?: null;

}

Enter fullscreen mode Exit fullscreen mode

Маппинг статусов

Статусы заказов и стадии сделок — это разные сущности. Нужен маппинг.

Пример маппинга:


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';

}

Enter fullscreen mode Exit fullscreen mode

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


Результат

После правильной настройки:

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

Вывод

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

Это:

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

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


Репозиторий

Исходный код с обезличенной реализацией, примерами и структурой проекта:

👉 https://github.com/va-proger/vp\_bitrix\_deal\_to\_order\_status\_sync

Read more on viku-lov.ru

Top comments (0)