Skip to content

ТЗ — CRM-коннекторы (cms/integration-crm)

Слой: 🟡 функц-модуль · Зрелость доноров: ★ · Донор: — Статус: ТЗ к разработке

Назначение и возможности

Коннекторы CRM-систем (Битрикс24, amoCRM, RetailCRM) поверх cms/integrations-bus: отправляют лиды и заказы во внешнюю CRM событийно, из очереди, с настраиваемым маппингом полей и приёмом статусов обратно через вебхуки.

  • Коннекторы Битрикс24, amoCRM, RetailCRM: provides: crm-connector
  • Отправка лидов/заказов в CRM асинхронно из очереди с ретраями через cms/integrations-bus
  • Настраиваемый маппинг полей (сайт → CRM) без правки кода, через Filament
  • Приём статусов сделки/заказа обратно через cms/webhooks-in
  • Дедупликация контактов по телефону/email — не создаёт дубль лида в CRM
  • Журнал синхронизации с диагностикой ошибок маппинга

Зависимости и выключение

requires: cms/integrations-bus, cms/webhooks-in · provides: crm-connector

Поведение при выключении: лиды и заказы перестают отправляться во внешнюю CRM и остаются только в сайте — обработка заявок продолжается через встроенные инструменты студии (cms/leads, если подключён), без потери данных.

Модель данных

ТаблицаКлючевые поляПримечание
cms_integration_crm_field_mappingsid, crm_code, source_field, target_fieldнастраиваемый маппинг полей
cms_integration_crm_sync_logid, crm_code, entity_type, entity_id, external_id, status, attemptsжурнал отправки лидов/заказов

Индексы: status — PHP Enum; уникальный индекс на crm_code+entity_type+entity_id — одновременно быстрый поиск по сущности и гарантия идемпотентности при параллельной отправке одного лида дважды (см. «Крайние случаи»); cms_integration_crm_sync_log — журнал, append-only, BRIN по created_at.

ПДн-паспорт. В CRM уходят: ФИО, телефон, email, комментарий лида/заказа, UTM-метки — ровно те поля, что настроены в cms_integration_crm_field_mappings. Срок хранения cms_integration_crm_sync_log — 12 месяцев (ретеншн-джоба cms:integration-crm:prune-log, регистрируется через ScheduleRegistrar), payload лида/заказа в самой CRM модуль не хранит и не обязан «забывать» (данные субъекта в CRM — вне периметра сайта; хук ядра «забыть по запросу» удаляет запись в sync_log, а не данные в CRM).

Входные и выходные данные

Входы (всё прочее модуль отвергает — whitelist-принцип §11 стандарта):

ВходИсточникКаналПроверка
Лид создансобытие LeadCreated (cms/core)1 — событиефакт от ядра, доп. валидации не требует
Заказ оплачен/сменил статуссобытие OrderPaid/OrderStatusChanged (cms/commerce-orders)1 — событиеобрабатывается только если sync_orders=true
Событие LeadStatusChanged (ядро) → lead, from, toядро1 — событиетриггер отправки смены статуса в CRM
Маппинг полей (форма Filament/API)админAPIFormRequest, whitelist полей источника/цели
Ручная переотправка (retry)админAPIpermission integration-crm.manage, Idempotency-Key
Вебхук статуса сделки/заказа из CRMвнешняя CRM → cms/webhooks-in5 — очередь (приём)подпись/токен коннектора верифицируется до постановки job; неверная подпись — 401, запись в sync-log не создаётся; проверка timestamp против replay (окно ядра cms/webhooks-in)

Выходы:

ВыходПолучательФормат
CrmLeadSynced / CrmSyncFailed / CrmStatusReceivedподписчики (канал 1)событие, payload см. «События и обмен»
Лид/сделка в CRMвнешняя CRM (через коннектор)payload по активному маппингу полей, только замапленные поля
Журнал синхронизацииадминкаcms_integration_crm_sync_log, keyset-пагинация
Отчёт ошибок маппингаадминказапись в sync-log со статусом mapping_error + детали

Любое поле payload, не описанное в cms_integration_crm_field_mappings, в CRM не уходит; любое событие, кроме перечисленных, модуль не слушает (нет подписки — не скрытая интеграция).

Настройки (группа integration-crm)

КлючТипДефолтaffectsPageCacheОписание
integration-crm.enabled_connectorsarray[]нетАктивные коды CRM
integration-crm.dedup_bystring"phone"нетПоле дедупликации контактов (phone/email)
integration-crm.sync_ordersbooltrueнетОтправлять заказы, не только лиды
integration-crm.retry_attemptsint3нетЧисло ретраев отправки при ошибке
integration-crm.batch_sizeint20нетМакс. число лидов в одном батч-запросе к CRM (там, где API это поддерживает)
integration-crm.rate_limit_per_minuteint60нетЛимит вызовов API на CRM — троттлинг очереди, защита от блокировки аккаунта
integration-crm.max_pending_queue_sizeint1000нетПорог записей в очереди на CRM; превышение — алерт cms/health, отправка не останавливается тихо
integration-crm.max_pending_age_hoursint24нетПорог возраста неотправленной записи; превышение — алерт cms/health
integration-crm.kill_switcharray[]нетКоды CRM с аварийно приостановленной синхронизацией — без выключения модуля и без потери маппинга

Секреты доступа к API CRM (токены, вебхук-URL) — только .env.

API

МетодПутьДоступНазначение
GET/api/v1/admin/integration-crm/mappingsadmin (integration-crm.manage)Настройка маппинга полей
PUT/api/v1/admin/integration-crm/mappingsadmin (integration-crm.manage)Сохранение маппинга полей
GET/api/v1/admin/integration-crm/sync-logadmin (integration-crm.view)Журнал синхронизации с фильтрами
POST/api/v1/admin/integration-crm/sync-log/{id}/retryadmin (integration-crm.manage)Повторная отправка вручную
POST/api/v1/admin/integration-crm/sync-log/retry-bulkadmin (integration-crm.manage)Массовая переотправка группы failed-записей (список id)

Журнал синхронизации — keyset-пагинация, не OFFSET. Оба retry-эндпоинта — мутация с внешним эффектом (повторная отправка в CRM) → обязателен заголовок Idempotency-Key (§7 стандарта, конверт ошибок — код mapping_error/rate_limited/connector_disabled в поле code).

Компоненты

Filament: форма маппинга полей, журнал синхронизации с ретраем. Команды: cms:integration-crm:retry-failed --json, cms:integration-crm:doctor --json.

События и обмен

СобытиеКогдаPayload
CrmLeadSyncedлид успешно отправлен в CRMcrm_code, lead_id, external_id
CrmSyncFailedотправка завершилась ошибкой после всех ретраевcrm_code, entity_type, entity_id, error
CrmStatusReceivedполучен статус сделки/заказа обратно из CRMcrm_code, external_id, status

Слушает: WebhookReceived из cms/webhooks-in для статусов из CRM; LeadStatusChanged (ядро) — триггер исходящей отправки смены статуса в CRM (с анти-эхо по origin, см. «Крайние случаи»). Реализует provides: crm-connector. Весь обмен с CRM — через cms/integrations-bus.

Взаимодействия

Сущность/модульКаналНаправлениеЧто происходит
LeadCreated (cms/core)1 — событиеcore → integration-crmлид ставится в очередь integration-crm на отправку
OrderPaid/OrderStatusChanged (cms/commerce-orders)1 — событиеcommerce-orders → integration-crmзаказ ставится в очередь, если sync_orders=true
cms/webhooks-in5 — очередь (приём)CRM → webhooks-in → integration-crmподписанный вебхук статуса кладёт job разбора; см. «Входные и выходные данные»
cms/integrations-bus, provides: crm-connector3 — provides-контрактintegration-crm → шинареализация способности «отправить лид/сделку», резолв по crm_code
cms/health1 — событие CrmSyncFailedintegration-crm → healthалерт при исчерпании ретраев/систематических ошибках/превышении max_pending_*
cms/leads (если подключён)— (связи нет)заявки на сайте обрабатываются независимо от статуса CRM-синхронизации

Фоновая работа

Отправка лидов/заказов — именованная очередь integration-crm, асинхронно, батчами по batch_size там, где API коннектора поддерживает батч-отправку (иначе по одной с троттлингом по rate_limit_per_minute), с ретраями (backoff) через cms/integrations-bus; circuit breaker на CRM при систематических ошибках (открывается — коннектор помечается degraded, алерт разработчику, другие коннекторы не затронуты). Слушатели событий тонкие — валидируют факт и кладут job, вся логика маппинга и дедупликации — в сервисе. Внешние HTTP-вызовы к CRM выполняются только из очереди — никогда синхронно в потоке ответа.

Ранбук (мини, §15 стандарта):

СимптомЧто проверитьКоманда
Очередь integration-crm растёт, CRM не отвечаетhealth-чек коннектора, max_pending_queue_size/max_pending_age_hours в алертах cms/healthcms:integration-crm:doctor --json; при затяжном сбое — kill_switch на код CRM
Массовая CrmSyncFailed с кодом mapping_errorворонка/поля в CRM изменились — сверить маппинг с текущей схемой CRMFilament: форма маппинга полей → пересохранить; cms:integration-crm:retry-failed --json после правки

Производительность и кеш

Ожидаемые объёмы (оценочно, типовой сайт студии): лиды — от десятков до ~2 000 в сутки, заказы (при sync_orders=true) — сопоставимо с суточным объёмом заказов сайта. Горячие пути: постановка лида/заказа в очередь (слушатель тонкий, ≤2 запроса: чтение кеша маппинга + insert job/лога) и приём вебхука статуса (подпись верифицирует cms/webhooks-in до контроллера модуля, сам обработчик ≤3 запроса: найти запись по crm_code+external_id, обновить статус, применить анти-эхо по origin). Индексы — см. «Модель данных» (crm_code+entity_type+entity_id, BRIN по created_at). Тег integration-crm:mappings — кеш маппинга полей на CRM, инвалидируется при сохранении настроек маппинга в Filament (0 запросов на горячем пути отправки). Внешние вызовы к CRM — только из очереди, никогда на горячем пути публичной страницы или синхронного ответа API.

Безопасность

  • Приём статуса из вебхука идемпотентен по event_id — повторная доставка не создаёт дублирующий переход статуса.
  • Дедупликация контактов по телефону/email не создаёт повторный лид при повторной отправке (поиск существующего контакта — до создания нового, см. «Крайние случаи»).
  • Вебхуки: подпись/токен каждого коннектора (amoCRM — X-Signature, Битрикс24 — токен приложения, RetailCRM — API-key) верифицируется на уровне cms/webhooks-in до постановки job; неверная подпись → 401, событие безопасности — в cms/audit/ cms/attack-monitor. Защита от replay — проверка timestamp вебхука против окна ядра (по умолчанию из настроек cms/webhooks-in).
  • Секреты доступа к API CRM — только .env, в БД/логах/UI не хранятся и не выводятся.
  • В логи и sync-log не попадают полные ПДн сверх необходимого — телефон/email логируются маскированно, полный payload доступен только через сам запрос к CRM.

Матрица ролей (permissions из манифеста):

Permissionadminменеджерредакторstudio
integration-crm.view (журнал, статусы, расход квоты API)
integration-crm.manage (маппинг полей, retry/bulk-retry, kill-switch коннектора)

Опасные действия (массовая переотправка, kill-switch) — только повышенные роли (admin/studio), с подтверждением в UI (см. «UX-требования»).

UX-требования

Админ:

  • статус синхронизации виден на карточке лида/заказа (бейдж: не отправлен / в очереди / отправлен / ошибка);
  • журнал обменов с фильтрами по CRM/статусу/дате, пустое состояние — «Синхронизация ещё не запускалась» со ссылкой на настройку маппинга;
  • ошибки — на человеческом языке, не техническим кодом: «amoCRM не отвечает, лид поставлен в очередь на отправку», «Битрикс24: превышен лимит запросов, повтор через N минут», «Поле "Источник" не найдено в текущей воронке amoCRM — проверьте маппинг» (не «Error 500» и не сырой стек-трейс);
  • массовые действия: чекбоксы в журнале для группового retry failed-записей, с подтверждением количества («Переотправить 42 записи?»);
  • подтверждение необратимых/рискованных операций: включение kill-switch при накопленной очереди — «N лидов не будут отправляться в CRM, продолжить?».

Посетитель: отправка формы лида никогда не ждёт ответа CRM (весь обмен асинхронный из очереди) — сбой CRM не виден на сайте и не блокирует приём заявки.

Крайние случаи и типовые баги

  • Дубли лидов → перед постановкой в очередь на создание сервис ищет существующий контакт в CRM по dedup_by (телефон/email, с fallback на второе поле, если первое пусто) до создания новой сущности; найден — сделка создаётся на существующий контакт, не создаётся новый; не найден — создаётся новый контакт. Постфактум-сверка не применяется, поиск — обязательный шаг перед созданием.
  • Двусторонний маппинг статусов создаёт петлю событий (сайт меняет статус → уходит в CRM → CRM шлёт вебхук с тем же статусом → сайт снова пытается отправить) → каждая запись cms_integration_crm_sync_log несёт origin (site/crm); исходящий job создаётся только при изменении статуса с origin=site; изменение, пришедшее с origin=crm и совпадающее с уже сохранённым статусом, не порождает исходящую отправку (анти-эхо).
  • Rate-limit провайдера (amoCRM/Битрикс24 жёстко лимитируют запросы) → батчинг отправки (batch_size) вместо отправки по одному там, где API это поддерживает; троттлинг очереди по rate_limit_per_minute, а не одновременный залп джобов.
  • CRM недоступна длительное время → очередь копится; max_pending_queue_size и max_pending_age_hours — явные пороги алерта в cms/health, не безлимитный рост; данные не теряются и не отбрасываются тихо, решение (ждать/kill-switch) — за админом.
  • Вебхук статуса пришёл на несуществующую/уже удалённую сделку → запись в лог со статусом unknown_deal + алерт при частоте выше нормы, ответ 200 (подтверждение приёма вебхука по контракту провайдера), не 500.
  • Конфликт маппинга полей — CRM вернула поле, которого нет в текущей настройке (изменилась схема воронки) → запись в журнал ошибок со статусом mapping_error и деталями расхождения, не блокирует синхронизацию остальных лидов/заказов.
  • Смена схемы ответа API CRM (breaking change у провайдера) → circuit breaker переводит коннектор в degraded, алерт разработчику через cms/health, другие подключённые CRM не затронуты.
  • Параллельная отправка одного лида дважды (гонка при повторной обработке очереди) → уникальный индекс crm_code+entity_type+entity_id в sync_log + WithoutOverlapping на job — идемпотентность, не дубль сделки в CRM.
  • Лид без телефона и email (нечем дедуплицировать) → создаётся новый контакт без попытки поиска, событие/запись помечается dedup_skipped — видно в журнале, не тихо.

Донорский код

Донор: — (новая разработка). Legacy-импорт (§16 стандарта): при переезде клиента со старой платформы, где уже велась синхронизация с той же CRM, cms:integration-crm:import- legacy --source=<профиль> переносит связку external_id (сущность сайта ↔ id в CRM) в cms_integration_crm_sync_log, чтобы после запуска модуль не создавал в CRM дублирующие контакты/сделки для уже синхронизированных лидов; сами лиды/заказы остаются в своих модулях (cms/leads, cms/commerce-orders) — импортируется только связка идентификаторов. Идемпотентно (повторный прогон обновляет связку, не дублирует), --dry-run с отчётом расхождений.

Тесты и приёмка

  • [ ] Мок API CRM: тест отправки лида, получения статуса, обработки ошибки с ретраем
  • [ ] Дедупликация по телефону/email не создаёт повторный лид при повторной отправке; тест явно проверяет, что поиск контакта происходит до создания нового
  • [ ] Маппинг полей применяется без изменения кода — только через Filament-настройку
  • [ ] Обработка статуса из вебхука идемпотентна по event_id
  • [ ] При выключении модуля заказы/лиды не теряются, обработка продолжается на сайте
  • [ ] Права integration-crm.manage разграничивают просмотр журнала и настройку маппинга
  • [ ] Контрактный набор cms-testing зелёный, пакет тестируется в testbench-изоляции
  • [ ] Feature-тест на каждый роут; тестовая БД только integration_crm_test, migrate:fresh запрещён
  • [ ] Тест деградации: недоступный коннектор → circuit breaker открывается, 200 + meta.degraded на связанных admin-эндпоинтах (не 5xx), алерт в cms/health
  • [ ] Тест на дубли вебхуков: повторная доставка одного event_id не создаёт второй переход статуса
  • [ ] Тест анти-эхо: статус, пришедший с origin=crm, не порождает исходящую отправку того же статуса обратно в CRM
  • [ ] Тест идемпотентности параллельной отправки: два одновременных job на один лид → одна запись в CRM, не дубль
  • [ ] Тест лимита очереди: превышение max_pending_queue_size/max_pending_age_hours → алерт cms/health, отправка не блокируется молча
  • [ ] Тест kill-switch: код CRM в integration-crm.kill_switch → исходящие job не создаются, входящие вебхуки по-прежнему принимаются и логируются
  • [ ] Тест на LeadStatusChanged (ядро): смена статуса лида с origin=site ставит job отправки статуса в CRM; смена, пришедшая с origin=crm, повторной отправки не порождает

Внутренняя база знаний студии