815 lines
57 KiB
Markdown
815 lines
57 KiB
Markdown
# module-07. Полная спецификация `bitrix-sync`
|
||
|
||
> Статус: целевая постановка первого функционального релиза CRM-синхронизации.
|
||
> Заменяет прежнюю спецификацию DB-connectivity stub.
|
||
> Исторический, неканонический концепт: [`archive/sync-service-concept.md`](../../archive/sync-service-concept.md); для реализации использовать только настоящий документ и architectory.
|
||
> Связанные контракты: [`arch-01-system-architecture.md`](../../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../../architectory/arch-02-api-contracts.md), [`module-01-api-backend.md`](../../VM1_app/documentation/module-01-api-backend.md), [`module-10-deployment-vm2.md`](module-10-deployment-vm2.md).
|
||
|
||
## 1. Назначение и границы
|
||
|
||
`bitrix-sync` асинхронно связывает пользователя HAN с Contact Битрикс24 и поддерживает согласованное состояние профиля без синхронной зависимости пользовательских API от CRM.
|
||
|
||
В первый релиз входят:
|
||
|
||
- `contact.map_or_create` — первичный поиск/создание Contact и сохранение связи;
|
||
- `contact.update` — передача принадлежащих App полей в уже связанный Contact;
|
||
- `contact.deactivate` — снятие признака активной регистрации и закрытие mapping;
|
||
- `contact.rebind` — управляемое исправление ошибочной связи с другим Contact;
|
||
- сигнал HTTP-webhook робота Contact → App и обновление локального профиля;
|
||
- инкрементальная reconciliation на случай потерянных webhook;
|
||
- business alerts по конфликтам через смарт-процесс Битрикс24;
|
||
- durable workflow, batch, rate limit, retry, technical DLQ, audit и observability.
|
||
|
||
Вне первого релиза:
|
||
|
||
- Lead/Deal;
|
||
- документы компании в профиле;
|
||
- автоматический merge CRM-дублей;
|
||
- публичный/manual replay HTTP API;
|
||
- backfill пользователей, зарегистрированных до cutover;
|
||
- полная reconciliation всех Contact; в первом релизе выполняется только инкрементальная сверка;
|
||
- создание `UserIdentity` или `ClientProfile`;
|
||
- использование OAuth credential `bitrix-local-app`.
|
||
|
||
## 2. Принципы
|
||
|
||
1. PostgreSQL — source of truth очереди, workflow и mapping; оперативная память не хранит единственное состояние сценария.
|
||
2. Delivery semantics — at-least-once. Идемпотентность обязательна для каждой внешней операции.
|
||
3. Сценарий хранится как state machine, а не как заранее созданный произвольный DAG атомарных команд.
|
||
4. `han_app.sync_queue` содержит бизнес-намерения App; `bitrix_sync.crm_commands` содержит конкретные вызовы CRM.
|
||
5. Один CRM-command соответствует одному подзапросу `batch`.
|
||
6. Телефон используется для поиска только при первичном создании связи.
|
||
7. После формирования связи Contact читается и обновляется только по `b24_id`.
|
||
8. Запрашиваются только необходимые поля.
|
||
9. Business conflicts и technical failures имеют разные журналы и каналы эскалации.
|
||
10. Секреты и PII не записываются в payload очередей, логи, traces и метрики.
|
||
11. Runtime `bitrix-sync` не выполняет HTTP-вызовов на ВМ1 и не направляет через неё inbound/outbound CRM traffic; взаимодействие с App идёт через managed PostgreSQL.
|
||
|
||
## 3. Владение данными и field mapping
|
||
|
||
### 3.1. Mastership
|
||
|
||
- `UserIdentity.phone_number` — master App/Keycloak.
|
||
- `ClientProfile.full_name` — master Битрикс24, источник `Contact.NAME`.
|
||
- `ClientProfile.citizenship` — master Битрикс24.
|
||
- `ClientProfile.email` — master Битрикс24.
|
||
- `user_id` и registration flag — служебные поля интеграции с логическими значениями `active`/`inactive`; в подтверждённом фильтре universal CRM `active` кодируется как `1`.
|
||
- App DB — локальный read model для UI.
|
||
- `ClientProfile.foreign_phone` не синхронизируется в первом релизе и не должен создавать `contact.update`.
|
||
|
||
После первичного map/create App не отправляет в CRM `full_name`, `citizenship` и `email`. Изменения этих полей приходят только через webhook/reconciliation и записываются с `SET LOCAL han.sync_suppress='true'`.
|
||
|
||
### 3.2. Non-secret env mapping
|
||
|
||
Физические имена полей конкретного портала задаются non-secret env:
|
||
|
||
```text
|
||
BITRIX_SYNC_CONTACT_USER_ID_FIELD=UF_CRM_1785934432398
|
||
BITRIX_SYNC_CONTACT_REGISTERED_FIELD=UF_CRM_1778692456
|
||
BITRIX_SYNC_CONTACT_CITIZENSHIP_FIELD=UF_CRM_1768493029
|
||
```
|
||
|
||
Значения обязательны при `BITRIX_SYNC_ENABLED=true`, проверяются на startup по допустимому формату имени поля. Изменение требует контролируемого restart.
|
||
|
||
Для методов universal CRM физическое имя `UF_CRM_<latin letters or digits>` детерминированно преобразуется в REST-имя `ufCrm_<latin letters or digits>`. В частности, `BITRIX_SYNC_CONTACT_REGISTERED_FIELD=UF_CRM_6a70c275346a7` используется в `crm.item.list` как `ufCrm_6a70c275346a7`. Произвольное изменение регистра или иной алгоритм преобразования запрещены; обе формы валидируются на startup.
|
||
|
||
### 3.3. Преобразования
|
||
|
||
- телефон принимается только в валидном российском E.164 `+7XXXXXXXXXX`;
|
||
- sync-service не исправляет `8...`, пробелы, скобки или дефисы;
|
||
- невалидный телефон — permanent business failure до исправления источника;
|
||
- поиск выполняется `crm.duplicate.findbycomm` с `type=PHONE`, `entity_type=CONTACT`;
|
||
- `full_name = Contact.NAME`; `LAST_NAME` и `SECOND_NAME` не конкатенируются;
|
||
- citizenship в App — display value элемента списка;
|
||
- Bitrix enum ID и момент загрузки справочника сохраняются в sync snapshot;
|
||
- email: первый синтаксически валидный элемент с `VALUE_TYPE=WORK`, иначе первый валидный элемент;
|
||
- отсутствие значения в CRM очищает соответствующее CRM-master поле App.
|
||
|
||
Справочник citizenship загружается через `crm.contact.userfield.list`/эквивалентный актуальный метод, кэшируется с TTL и обновляется при неизвестном enum ID. Неизвестный ID не превращается в пустое значение: команда retry один раз после refresh, затем создаётся business alert.
|
||
|
||
## 4. Идентичность и mapping
|
||
|
||
Каноническая связь хранится только в `bitrix_sync.entity_external_mapping`. Схема `han_app` и `ClientProfile` не хранят `b24_id`: для App это внутренняя деталь интеграции.
|
||
|
||
Инварианты:
|
||
|
||
- у одного active `user_id` не более одного active Contact mapping;
|
||
- один `b24_id` не может быть активным для двух active пользователей;
|
||
- исторические mapping не удаляются;
|
||
- `user_id` в CRM содержит UUID `UserIdentity.id`;
|
||
- закрытие mapping фиксирует `closed_at`, `close_reason` и correlation/workflow ID;
|
||
- mapping, rebind workflow и их аудит принадлежат исключительно `bitrix-sync`.
|
||
|
||
Перед телефонным поиском workflow проверяет active mapping по `user_id`. Если mapping отсутствует, выполняется поиск по телефону. Если среди найденных Contact уже есть Contact с тем же `user_id`, локальный mapping восстанавливается без повторного create; CRM update допускается только для отличающегося registration flag.
|
||
|
||
## 5. Сценарии
|
||
|
||
### 5.1. `contact.map_or_create`
|
||
|
||
Вход: eligible active `user_id` (identity и profile находятся в допускающем синхронизацию active status), задача из `han_app.sync_queue`.
|
||
|
||
Общий порядок:
|
||
|
||
1. Получить актуальные `UserIdentity` и `ClientProfile`.
|
||
2. Проверить active mapping.
|
||
3. При отсутствии mapping выполнить `crm.duplicate.findbycomm` по телефону.
|
||
4. Получить только поля найденных Contact: ID, NAME, PHONE, EMAIL, citizenship, `user_id`, registration flag, CREATED_TIME и source update timestamp.
|
||
5. Если среди результатов есть Contact с тем же `user_id`, восстановить локальный mapping без повторного create, обновить registration flag только при расхождении и перейти к шагу 9.
|
||
6. Применить ветку В0–В2 (если на предыдущем шаге не завершен сценарий).
|
||
7. Обновить Contact.
|
||
8. Создать active mapping в `bitrix_sync`.
|
||
9. Для существующего/восстановленного Contact записать его CRM-master поля в App с `SET LOCAL han.sync_suppress='true'` и сохранить source snapshot; для созданного Contact сохранить snapshot фактически записанных значений.
|
||
10. При наличии конфликта создать/обновить business alert.
|
||
11. Завершить workflow и исходную queue task.
|
||
|
||
Ветки:
|
||
|
||
- **В0, Contact не найден:** создать Contact с телефоном, доступными начальными значениями, `user_id`, registration flag `active`; после ambiguous timeout повторить поиск по телефону и не повторять create вслепую.
|
||
- **В1.1, один Contact без active связи:** записать `user_id`, flag `active`, создать mapping.
|
||
- **В1.2, один Contact связан с другим active user:** создать новый Contact для текущего пользователя и business alert.
|
||
- **В2.1, несколько Contact, самый новый свободен:** выбрать самый новый по `CREATED_TIME`, затем по числовому ID как deterministic tie-breaker; создать mapping и alert о дублях.
|
||
- **В2.2, несколько Contact, выбранный связан с другим active user:** создать новый Contact и alert с полным перечнем candidate ID.
|
||
|
||
Если найден Contact со старым `user_id` неактивного/удалённого пользователя:
|
||
|
||
- закрыть старый active mapping, если он ещё открыт;
|
||
- записать новый `user_id` в тот же Contact;
|
||
- создать новый active mapping;
|
||
- сохранить историю прежней связи.
|
||
|
||
### 5.2. `contact.update`
|
||
|
||
Вход: изменение App-master данных уже связанного active пользователя. Исключая изменения, связанные с обновлением данных контакта в Битрикс.
|
||
|
||
- Найти Contact только по active mapping.
|
||
- Отправлять только изменившиеся App-master поля: в первом релизе телефон и при необходимости служебные поля.
|
||
- При отсутствии mapping не выполнять телефонный поиск: создать/coalesce `contact.map_or_create`.
|
||
- При `crm.contact.get/update` not found пометить mapping `broken`, сохранить последний profile snapshot и создать business alert.
|
||
|
||
### 5.3. `contact.deactivate`
|
||
|
||
Триггер: переход `UserIdentity.record_status` или `ClientProfile.record_status` из active в inactive/deleted.
|
||
|
||
- Найти Contact по active mapping.
|
||
- Установить registration flag `inactive`.
|
||
- Закрыть mapping с причиной деактивации.
|
||
- Не удалять Contact и историю mapping.
|
||
- Если Contact уже отсутствует, закрыть mapping как `broken/deactivated`, сохранить profile snapshot и создать alert.
|
||
|
||
### 5.4. `contact.rebind`
|
||
|
||
Перепривязка не выполняется непосредственным `UPDATE` mapping. Администратор вызывает audited procedure:
|
||
|
||
```text
|
||
bitrix_sync.request_bitrix_contact_rebind(
|
||
user_id,
|
||
target_b24_id,
|
||
reason,
|
||
operator_id
|
||
)
|
||
```
|
||
|
||
Процедура:
|
||
|
||
1. блокирует текущий active mapping пользователя;
|
||
2. проверяет отсутствие active mapping целевого `b24_id` с другим пользователем;
|
||
3. создаёт `rebind_request` и workflow `contact.rebind`;
|
||
4. не изменяет действующий mapping до завершения CRM-команд.
|
||
|
||
Worker:
|
||
|
||
1. читает прежний и целевой Contact по ID;
|
||
2. повторно проверяет, что целевой Contact существует и не содержит `user_id` другого active пользователя;
|
||
3. записывает в целевой Contact текущий `user_id` и registration flag `active`;
|
||
4. очищает `user_id` и устанавливает flag `inactive` в прежнем Contact, только если там всё ещё находится ожидаемый `user_id`;
|
||
5. после успеха обеих CRM-команд одной транзакцией закрывает старый mapping, создаёт новый active mapping и завершает `rebind_request`;
|
||
6. завершает связанный business alert только после успешного изменения mapping.
|
||
|
||
Если обновление целевого Contact прошло, а очистка прежнего завершилась временной ошибкой, workflow остаётся `waiting_retry`: новый mapping ещё не активируется, повторная команда проверяет текущее значение и безопасно продолжает сценарий. Все шаги идемпотентны.
|
||
|
||
### 5.5. Bitrix24 → App
|
||
|
||
1. Webhook принимается и durable сохраняется в `webhook_inbox`.
|
||
2. Несколько необработанных событий одного Contact coalesce в одно чтение.
|
||
3. По `b24_id` ищется active mapping.
|
||
4. Contact запрашивается по ID с минимальным `select`.
|
||
5. Проверяются `user_id` и registration flag.
|
||
6. CRM-master поля записываются в App одной транзакцией с `SET LOCAL han.sync_suppress='true'`.
|
||
7. Обновляются `source_updated_at`, sync snapshot и webhook status.
|
||
|
||
Webhook для неизвестного/неактивного mapping подтверждается без изменения App и фиксируется low-cardinality audit. Несовпадение `user_id` с active mapping создаёт business alert и не перезаписывает профиль.
|
||
|
||
Если Contact не найден:
|
||
|
||
- mapping становится `broken`;
|
||
- последний snapshot и UI-данные сохраняются;
|
||
- профиль помечается stale через sync metadata, а не очищается;
|
||
- создаётся business alert.
|
||
|
||
## 6. PostgreSQL-контракты
|
||
|
||
### 6.1. `han_app.sync_queue`
|
||
|
||
Существующая таблица мигрируется без потери записей. Требуемые поля:
|
||
|
||
```text
|
||
id uuid PK
|
||
task_type varchar(64)
|
||
entity_type varchar(64)
|
||
entity_id uuid
|
||
dedup_key varchar(255)
|
||
payload_json jsonb
|
||
status varchar(16)
|
||
attempt_count integer
|
||
next_attempt_at timestamptz
|
||
locked_by varchar(128) NULL
|
||
locked_until timestamptz NULL
|
||
lease_token uuid NULL
|
||
last_error_code varchar(64) NULL
|
||
last_error_at timestamptz NULL
|
||
completed_at timestamptz NULL
|
||
cancel_reason varchar(255) NULL
|
||
created_at timestamptz
|
||
updated_at timestamptz
|
||
```
|
||
|
||
Допустимые статусы:
|
||
|
||
```text
|
||
pending | leased | processed | retry_wait | dead_letter | cancelled
|
||
```
|
||
|
||
Индексы:
|
||
|
||
- `(status, next_attempt_at, created_at)` для claim;
|
||
- `(locked_until) WHERE status='leased'`;
|
||
- `(entity_type, entity_id, created_at DESC)`;
|
||
- partial unique dedup только для активных состояний `pending|leased|retry_wait`.
|
||
|
||
Глобальная `UNIQUE(dedup_key)` удаляется. Завершённая, отменённая или dead-letter задача не должна навсегда запрещать новое бизнес-событие.
|
||
|
||
Claim выполняется короткой транзакцией через `FOR UPDATE SKIP LOCKED`, выставляет `locked_by`, `locked_until`, новый `lease_token`. Любое завершение проверяет тот же token; просроченный worker не может подтвердить чужую lease.
|
||
|
||
### 6.2. Trigger contract
|
||
|
||
`han_app.enqueue_contact_sync()`:
|
||
|
||
- всегда использует `UserIdentity.id` как `entity_id/user_id`;
|
||
- проверяет `han.sync_suppress`;
|
||
- не создаёт задачу при `IS NOT DISTINCT FROM` для фактически отслеживаемых полей;
|
||
- insert active identity/profile создаёт/coalesce `contact.map_or_create`; trigger не читает схему `bitrix_sync`, наличие mapping проверяет worker;
|
||
- изменение `UserIdentity.phone_number` создаёт `contact.update`;
|
||
- переход любого record status из active создаёт `contact.deactivate`;
|
||
- возврат в active создаёт/coalesce `contact.map_or_create`;
|
||
- изменения CRM-master profile fields сами по себе не создают App→CRM задачу;
|
||
- trigger и business update находятся в одной транзакции.
|
||
|
||
Payload содержит только идентификаторы и безопасные версии, но не копии PII:
|
||
|
||
```json
|
||
{
|
||
"schema_version": 1,
|
||
"user_id": "uuid",
|
||
"reason": "identity_phone_changed",
|
||
"source_updated_at": "2026-08-05T10:00:00Z"
|
||
}
|
||
```
|
||
|
||
Worker всегда читает актуальное состояние таблиц; payload не считается snapshot профиля.
|
||
|
||
### 6.3. `bitrix_sync.entity_external_mapping`
|
||
|
||
Таблица принадлежит migration/runtime boundary `bitrix-sync`:
|
||
|
||
```text
|
||
id uuid PK
|
||
entity_type varchar(64)
|
||
entity_id uuid
|
||
external_system varchar(32) = 'bitrix24'
|
||
external_entity_type varchar(32) = 'contact'
|
||
external_id varchar(128)
|
||
status varchar(16) -- active|closed|broken
|
||
opened_at timestamptz
|
||
closed_at timestamptz NULL
|
||
close_reason varchar(64) NULL
|
||
workflow_id uuid NULL
|
||
created_at timestamptz
|
||
updated_at timestamptz
|
||
```
|
||
|
||
Ограничения:
|
||
|
||
- partial unique active `(external_system, entity_type, entity_id)`;
|
||
- partial unique active `(external_system, external_entity_type, external_id)`;
|
||
- active row не имеет `closed_at`;
|
||
- closed/broken row имеет `closed_at` или документированную причину broken.
|
||
|
||
Администратору не выдаётся произвольный write на таблицу. Procedure `bitrix_sync.request_bitrix_contact_rebind(...)` создаёт durable запрос и workflow, но не изменяет mapping непосредственно.
|
||
|
||
Существующая `han_app.entity_external_mapping` мигрируется:
|
||
|
||
1. создать целевую таблицу и ограничения в `bitrix_sync`;
|
||
2. перенести и проверить существующие связи;
|
||
3. переключить worker/repositories;
|
||
4. удалить GRANT и зависимости App от старой таблицы;
|
||
5. удалить `han_app.entity_external_mapping`;
|
||
6. отдельной contract migration удалить partial index и колонку `ClientProfile.bitrix_contact_id`.
|
||
|
||
До contract-фазы запрещено поддерживать две writable копии mapping.
|
||
|
||
### 6.4. Таблицы `bitrix_sync`
|
||
|
||
Минимальный набор:
|
||
|
||
- `workflow_instances` — тип, user/contact ID, state, current step, source task, deadline, outcome, timestamps;
|
||
- `crm_commands` — workflow, command type, safe request params, status, attempt counters, lease, batch/correlation ID, safe response projection;
|
||
- `webhook_inbox` — receiver type, event type, optional event ID/source timestamp, entity IDs, received/status timestamps, optional dedup fingerprint и source IP audit metadata;
|
||
- `business_alerts` — sequence number, fingerprint, type, severity, app user, Contact candidates, selected ID, Bitrix smart-process item ID/stage, occurrence count;
|
||
- `rebind_requests` — user ID, прежний/целевой Contact ID, reason/operator, workflow, status и audit timestamps;
|
||
- `contact_snapshots` — IDs, field hashes/versions, citizenship enum ID, CRM source timestamps, last applied source (`webhook|reconciliation|app_create`) и last webhook received/source timestamps; без бесконтрольного дублирования PII;
|
||
- `settings` и `settings_versions` — runtime business/worker settings с типом, validation status, version и audit;
|
||
- `technical_dead_letters` — operation/workflow, safe error code, attempt/deadline metadata;
|
||
- `reconciliation_cursors` — job type, watermark, overlap и last success.
|
||
|
||
`crm_commands.command_type` — закрытый code enum/CHECK. БД не хранит произвольные REST method names или шаблоны исполняемых запросов.
|
||
|
||
## 7. State machines
|
||
|
||
### 7.1. Workflow
|
||
|
||
```text
|
||
created
|
||
-> running
|
||
-> waiting_crm
|
||
-> waiting_retry
|
||
-> waiting_manual
|
||
-> succeeded | failed | cancelled
|
||
```
|
||
|
||
Только persisted transition активирует следующий шаг. Один workflow имеет не более одной активной команды, кроме явно независимых операций alert/status.
|
||
|
||
### 7.2. CRM command
|
||
|
||
```text
|
||
pending -> leased -> in_flight -> succeeded
|
||
\-> retry_wait -> pending
|
||
\-> uncertain -> reconcile -> succeeded|retry_wait|dead_letter
|
||
\-> dead_letter
|
||
```
|
||
|
||
`uncertain` обязателен для timeout после отправки create/update. Для create reconciliation повторяет поиск по телефону и среди результатов в первую очередь проверяет Contact с тем же `user_id`.
|
||
|
||
### 7.3. Webhook
|
||
|
||
```text
|
||
received -> coalesced|processing -> processed
|
||
\-> retry_wait -> processing
|
||
\-> dead_letter
|
||
```
|
||
|
||
## 8. Worker, batching и rate limit
|
||
|
||
Одна replica первого релиза, но все claims и leases безопасны для будущих нескольких replica.
|
||
|
||
- PostgreSQL `LISTEN/NOTIFY` используется только как wake-up optimization;
|
||
- резервный polling обязателен;
|
||
- batch flush: достижение configured size либо configured max wait;
|
||
- default batch size `20`, допустимый диапазон `1..50`;
|
||
- один HTTP batch может содержать команды разных workflow, если они независимы;
|
||
- результат каждого подзапроса разбирается отдельно;
|
||
- FIFO определяется `next_attempt_at, created_at`, но retry не блокирует новые задачи;
|
||
- общий token bucket на portal/credential;
|
||
- безопасный default `2` HTTP requests/sec до подтверждения тарифа;
|
||
- burst и refill конфигурируются;
|
||
- default max in-flight HTTP requests `2`;
|
||
- медленный запрос не блокирует новый, пока доступен token и in-flight slot;
|
||
- несколько replica координируют limiter через PostgreSQL; локальный limiter допустим только при одной replica и явном readiness guard.
|
||
|
||
## 9. Retry и DLQ
|
||
|
||
Transient:
|
||
|
||
- connect/read timeout;
|
||
- временный DNS/TLS/network failure;
|
||
- HTTP 408/429/5xx;
|
||
- `QUERY_LIMIT_EXCEEDED`;
|
||
- временная недоступность PostgreSQL.
|
||
|
||
Политика: exponential backoff с full jitter, `Retry-After`/`operating_reset_at` приоритетнее локального delay, общий retry horizon 24 часа.
|
||
|
||
`QUERY_LIMIT_EXCEEDED` не расходует обычный business-attempt budget: он уменьшает limiter rate и переносит команду. `OPERATION_TIME_LIMIT` блокирует только затронутый method class до reset.
|
||
|
||
Permanent:
|
||
|
||
- invalid field/configuration;
|
||
- permission denied;
|
||
- credential rejected;
|
||
- нарушенный DB invariant.
|
||
|
||
Malformed App/CRM business data, включая невалидный телефон и неизвестный citizenship enum после refresh, переводит workflow в `waiting_manual` и создаёт/coalesce business alert; это не technical DLQ. Permanent technical failure немедленно попадает в technical DLQ. Credential/config/permission failure переводит readiness в not-ready или degraded по матрице и создаёт SigNoz alert. Задача в смарт-процессе для technical failures не создаётся.
|
||
|
||
В лог/БД сохраняются safe error code, HTTP status, Bitrix error code, attempt/deadline и correlation ID. Сырой response и exception text проходят redaction; утверждение «внутренний сервис» не разрешает логировать PII или секреты.
|
||
|
||
## 10. Webhook receivers
|
||
|
||
### 10.1. Адреса приёма
|
||
|
||
```text
|
||
Contact receiver:
|
||
https://<processing-public-host>/bitrix/sync/webhook/contact?token=<contact-receiver-token>
|
||
|
||
Alert receiver:
|
||
https://<processing-public-host>/bitrix/sync/webhook/alert?token=<alert-receiver-token>
|
||
```
|
||
|
||
Эти адреса задаются в HTTP-webhook роботах Битрикс24 и ведут прямо в receiver routes `bitrix-sync`. Это не callbacks local app и не Bitrix event handlers. Установка local app, `event.bind`, OAuth и `application_token` для CRM sync не используются; `auth[member_id]` служит только дополнительной проверкой источника webhook.
|
||
|
||
Фактический контракт штатного HTTP-webhook робота Битрикс24:
|
||
|
||
```text
|
||
POST <receiver>?token=<receiver-token>&ID=<entity-id>
|
||
Content-Type: application/x-www-form-urlencoded
|
||
|
||
document_id[0]=crm
|
||
document_id[1]=CCrmDocumentContact
|
||
document_id[2]=CONTACT_<contact-id>
|
||
auth[domain]=<portal-host>
|
||
auth[client_endpoint]=https://<portal-host>/rest/
|
||
auth[server_endpoint]=https://oauth.bitrix24.tech/rest/
|
||
auth[member_id]=<portal-member-id>
|
||
```
|
||
|
||
Для alert `document_id[1]` содержит тип dynamic document, а `document_id[2]=DYNAMIC_<expected-entity-type-id>_<item-id>`. Значение query `ID` должно совпадать с ID из `document_id[2]`; расхождение отклоняется как malformed request.
|
||
|
||
Штатный робот не позволяет передать Bearer header, поэтому отдельный высокоэнтропийный Contact/alert receiver token передаётся только в query. Это принятое ограничение платформы, а не предпочтительный способ аутентификации. Токены различаются между receiver и окружениями, сравниваются constant-time и поддерживают контролируемую ротацию с коротким периодом перекрытия.
|
||
|
||
Query token не должен попадать в access/error/audit logs, traces, метрики, Referer или diagnostic response. Nginx и приложение логируют нормализованный route без `$request_uri`/query; redirect для receiver routes запрещён. Примеры, fixtures и документация используют только placeholder, реальные значения из наблюдений должны быть отозваны, если они когда-либо сохранялись вне secret manager.
|
||
|
||
Публичный nginx ВМ2:
|
||
|
||
- принимает только HTTPS;
|
||
- ограничивает body, request rate и методы;
|
||
- до проксирования проверяет source IP TCP-соединения по явно заданному allow-list CIDR и возвращает `403` для остальных адресов;
|
||
- проксирует в локальный upstream `bitrix-sync` по закрытой Docker network;
|
||
- не логирует body/query secrets;
|
||
- не публикует internal status;
|
||
- возвращает generic errors без stack/IDs.
|
||
|
||
Allow-list применяется к адресу непосредственного peer, а не к недоверенному `X-Forwarded-For`. Если перед nginx появляется внешний trusted proxy/LB, схема извлечения real IP и список trusted proxy должны пройти отдельный deployment review. Автоматическое добавление IP по входящему запросу запрещено.
|
||
|
||
Для отклонённых по IP запросов сохраняются только timestamp, source IP, receiver route и outcome с ограниченным retention; query и body не сохраняются. Счётчики агрегируются по receiver и причине без IP label. Source IP разрешается использовать только для диагностики и контролируемого обновления allow-list.
|
||
|
||
Nginx/его telemetry pipeline экспортирует отклонения в общую observability как `webhook_rejected_total{receiver,reason="source_ip"}`; приложение не может сформировать эту метрику, поскольку запрещённый запрос до upstream не доходит.
|
||
|
||
Bitrix24 обращается непосредственно к отдельному public host ВМ2. ВМ1 не принимает и не проксирует CRM webhook, не хранит его TLS/route configuration и не является runtime-зависимостью `bitrix-sync`.
|
||
|
||
До durable insert receiver проверяет query token, `POST`, `application/x-www-form-urlencoded`, bounded body/query, допустимый document type, entity ID и его совпадение в query/body, ожидаемый alert `entity_type_id`, portal domain и `member_id`. Переданные `client_endpoint`/`server_endpoint` не используются как адреса исходящих запросов. Неверная аутентификация или source IP возвращает `403`, malformed contract — `400`; такие запросы не создают inbox row.
|
||
|
||
Робот не передаёт достоверные event ID и occurred timestamp. `received_at` назначается сервером, а порядок и актуальность определяются последующим чтением Contact и нормализованным CRM source timestamp. Каноническое поле universal CRM — `updatedTime`; если точечное чтение выполняется legacy-методом `crm.contact.get`, его `DATE_MODIFY` преобразуется в тот же `source_updated_at` только после contract test эквивалентности. Для событий без стабильного event ID постоянная уникальность по fingerprint не применяется: несколько pending событий одного receiver/entity coalesce в одно чтение, а повторная обработка безопасна по Contact ID и source timestamp. Если Bitrix позднее предоставит стабильный event ID, он может использоваться для точной дедупликации.
|
||
|
||
После успешного durable insert возвращается `200/202`; бизнес-обработка в HTTP request не выполняется.
|
||
|
||
HTTP-webhook робота не считается гарантированной доставкой, если в портале явно не подтверждена retry policy; поэтому reconciliation обязательна.
|
||
|
||
### 10.2. Contact reconciliation
|
||
|
||
Для active mapped Contact выполняется инкрементальная сверка:
|
||
|
||
- `crm.item.list` с `entityTypeId=3` (Contact);
|
||
- фильтр `>=updatedTime: watermark-overlap`, `opened: 1` и `<registered-field-rest-name>: 1`;
|
||
- первый проход выбирает только `id`, затем изменившиеся active mapped Contact читаются по ID с минимальным набором CRM-master и служебных полей;
|
||
- pagination и batch;
|
||
- watermark двигается только после полного успешного инкрементального run, а не после отдельной страницы;
|
||
- overlap обеспечивает повторное чтение границы;
|
||
- повтор безопасен по Contact ID/source timestamp;
|
||
- default interval 15 минут;
|
||
- рассчитано на объём до 10 000 active Contact.
|
||
|
||
Канонический запрос поиска кандидатов отправляется `POST` к REST method `crm.item.list` с `Content-Type: application/json` и `Accept: application/json`:
|
||
|
||
```json
|
||
{
|
||
"entityTypeId": 3,
|
||
"select": ["id"],
|
||
"filter": {
|
||
">=updatedTime": "2026-08-05T09:00:00",
|
||
"opened": 1,
|
||
"ufCrm_1778692456": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
Timestamp в запросе — вычисленный `watermark-overlap`, а имя custom field — REST-форма `BITRIX_SYNC_CONTACT_REGISTERED_FIELD`; приведённые значения являются примером контракта. Фильтры `opened=1` и registration flag обязательны: reconciliation не сканирует все Contact портала.
|
||
|
||
Wire-значения записи registration flag через выбранный add/update method проверяются отдельным contract test портала. Логика сервиса не смешивает `active`/`inactive` с конкретным представлением `1/0` или `Y/N`; repository adapter выполняет подтверждённое преобразование.
|
||
|
||
Полная сверка всех Contact в первом релизе не выполняется и не планируется по расписанию. Решение о её внедрении принимается отдельно по production-метрикам потерь и расхождений.
|
||
|
||
Каждый инкрементальный run по source attribution и timestamp в `contact_snapshots` считает Contact, для которых CRM-master состояние оказалось новее локального snapshot и было восстановлено reconciliation без ранее обработанного webhook. Всплеск абсолютного числа или доли таких Contact относительно настроенного порога/обычного baseline создаёт ops alert о возможной потере webhook.
|
||
|
||
Runbook по этому alert:
|
||
|
||
1. сравнить время всплеска с `webhook_rejected_total{reason="source_ip"}` и безопасным журналом отклонённых source IP;
|
||
2. проверить, что новый адрес действительно принадлежит инфраструктуре Битрикс24/портала, используя согласованный канал или контролируемый probe; одного факта запроса с корректным token недостаточно;
|
||
3. при подтверждении изменить version-controlled allow-list CIDR, пройти review и применить конфигурацию nginx;
|
||
4. убедиться, что webhook снова принимаются, а последующие инкрементальные run не находят растущих расхождений.
|
||
|
||
Allow-list никогда не расширяется автоматически по метрике или журналу. Инкрементальная reconciliation восстанавливает пропущенные изменения, поэтому блокировка нового легитимного IP ухудшает latency, но не должна приводить к окончательной потере согласованности.
|
||
|
||
### 10.3. Alert reconciliation
|
||
|
||
Изменение элемента смарт-процесса приходит через alert HTTP-webhook робот. Дополнительно открытые alerts сверяются batch-poll:
|
||
|
||
- default каждые 60 минут;
|
||
- только незавершённые item ID;
|
||
- terminal stage закрывает локальный alert;
|
||
- удалённый item переводит alert в `remote_missing` и создаёт ops/business signal согласно типу.
|
||
|
||
## 11. Business alerts
|
||
|
||
Смарт-процесс «Конфликты синхронизации» создаётся до production enablement.
|
||
|
||
Параметры хранятся в versioned `bitrix_sync.settings`:
|
||
|
||
- `entity_type_id`;
|
||
- category/pipeline ID;
|
||
- stage IDs `new`, `in_progress`, `resolved`, `closed_without_resolution`;
|
||
- field IDs;
|
||
- responsible user/group;
|
||
- reconciliation interval;
|
||
- SLA один рабочий день.
|
||
|
||
Обязательные поля элемента:
|
||
|
||
- внутренний `alert_number`;
|
||
- fingerprint;
|
||
- type/severity;
|
||
- app user ID;
|
||
- current/selected/candidate Contact IDs;
|
||
- masked details;
|
||
- occurrence count;
|
||
- first/last occurrence;
|
||
- previous alert link;
|
||
- service correlation ID.
|
||
|
||
Один открытый alert существует на `(type, fingerprint)`. Повтор увеличивает `occurrence_count` и обновляет item. После terminal stage новый случай создаёт новый alert со ссылкой на предыдущий.
|
||
|
||
После разбора конфликта администратор запускает `bitrix_sync.request_bitrix_contact_rebind(...)`. Простая смена стадии alert не изменяет mapping. Sync-service переводит alert в terminal stage только после успешного `contact.rebind`; произвольный SQL запрещён.
|
||
|
||
## 12. Настройки
|
||
|
||
### 12.1. Runtime secrets
|
||
|
||
Обязательный каталог для secret manager:
|
||
|
||
```text
|
||
BITRIX_SYNC_DATABASE_URL
|
||
BITRIX_SYNC_CRM_REST_WEBHOOK_URL
|
||
BITRIX_SYNC_CONTACT_RECEIVER_TOKEN
|
||
BITRIX_SYNC_ALERT_RECEIVER_TOKEN
|
||
BITRIX_SYNC_SERVICE_TOKEN
|
||
```
|
||
|
||
`BITRIX_SYNC_CRM_REST_WEBHOOK_URL` — credential-bearing URL входящего webhook Битрикс24, через который sync-service вызывает CRM REST. Он не связан с двумя public receiver URLs ВМ2 и считается единым секретом. Сервис не собирает его из логируемых частей. Receiver tokens также остаются secret-manager values, хотя из-за ограничения робота подставляются в query настроенных URL; доступ к конфигурации роботов Битрикс24 должен быть ограничен.
|
||
|
||
Секреты доставляются существующим `han-secrets` из Selectel Secrets Manager в read-only files `/run/han-chat/secrets`; fallback для no-egress VM — root-owned files. Они не хранятся в `.env` или БД.
|
||
|
||
### 12.2. Non-secret env
|
||
|
||
```text
|
||
BITRIX_SYNC_ENABLED=true
|
||
BITRIX_SYNC_MODE=full
|
||
BITRIX_SYNC_CONTACT_USER_ID_FIELD=UF_CRM_...
|
||
BITRIX_SYNC_CONTACT_REGISTERED_FIELD=UF_CRM_1778692456
|
||
BITRIX_SYNC_CONTACT_CITIZENSHIP_FIELD=UF_CRM_...
|
||
BITRIX_SYNC_PORTAL_HOST=<approved-host>
|
||
BITRIX_SYNC_PORTAL_MEMBER_ID=<approved-member-id>
|
||
BITRIX_SYNC_PUBLIC_BASE_URL=https://<processing-public-host>
|
||
BITRIX_WEBHOOK_ALLOWED_CIDRS=<comma-separated-cidrs>
|
||
BITRIX_SYNC_HTTP_TIMEOUT_SEC=10
|
||
BITRIX_SYNC_DB_POOL_SIZE=5
|
||
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
||
SECRETS_SOURCE=selectel
|
||
```
|
||
|
||
Portal host обязан совпадать с host secret URL, а portal member ID — с `auth[member_id]` входящих webhook; redirect на другой host запрещён. `BITRIX_WEBHOOK_ALLOWED_CIDRS` применяется deployment/nginx, проходит CIDR validation и version-controlled review; пустой allow-list при enabled receiver является ошибкой deployment-конфигурации. Nginx config test и проверка allow-list входят в preflight, поскольку application readiness не может подтвердить фактически загруженную конфигурацию внешнего proxy.
|
||
|
||
`BITRIX_SYNC_MODE=full` означает реализацию полного набора сценариев этого модуля, а не запуск full reconciliation. Существующие stub-era `.env.example`, Compose и `deployment/secrets/config.example.json` не являются контрактом full sync. В реализации они обновляются одновременно с кодом и manifest validation; до этого `BITRIX_SYNC_ENABLED=false`. В частности, api-backend не является consumer `BITRIX_SYNC_SERVICE_TOKEN`, а устаревшие `BITRIX_SYNC_CRM_BASE_URL`, `CONTACT_*_INTERVAL_SEC` и `BITRIX_SYNC_CRM_MAX_CONCURRENCY` не переносятся в full mode.
|
||
|
||
### 12.3. Hot settings
|
||
|
||
В `bitrix_sync.settings`: batch size/wait, claim size, lease TTL, limiter refill/burst, max in-flight, retry base/max/horizon, reconciliation intervals, пороги всплеска восстановленных reconciliation Contact, alert process IDs/stages/fields/SLA.
|
||
|
||
Worker читает active validated version, кэширует её и периодически проверяет version. Невалидная новая версия не активируется. Secrets, DSN, field names, portal host/member ID, inbound CIDR allow-list и cutover mode не меняются hot.
|
||
|
||
## 13. Минимальные права
|
||
|
||
Технический пользователь входящего webhook Б24:
|
||
- read Contact и перечисленных полей;
|
||
- add Contact;
|
||
- update только Contact в разрешённом CRM scope;
|
||
- read userfield metadata для citizenship;
|
||
- add/read/update элементы конкретного smart process alerts;
|
||
- без Lead/Deal delete/export/admin прав;
|
||
- без доступа к чатам, документам, телефонии и настройкам портала.
|
||
|
||
Точные ограничения проверяются smoke вызовами до enablement. Использование webhook администратора с полными правами запрещено.
|
||
|
||
DB role `bitrix_sync_user`:
|
||
- owner/write собственной схемы `bitrix_sync`;
|
||
- `SELECT, UPDATE` только необходимых колонок `han_app.sync_queue`;
|
||
- `SELECT` нужных колонок identity/profile;
|
||
- controlled `UPDATE` только CRM-master полей профиля и `source_updated_at`;
|
||
- без broad schema write, DDL и доступа к сообщениям/согласиям/документам;
|
||
- mapping/rebind tables находятся в собственной схеме и не требуют write GRANT на `han_app`;
|
||
- запись профиля App выполняется только через repository с transaction-local GUC.
|
||
|
||
## 14. HTTP health/status
|
||
|
||
- `GET /health/live`: process/event loop жив, без DB/CRM вызова.
|
||
- `GET /health/ready`: validated application config/secrets, PostgreSQL доступен, queue grants работают, workers/limiter живы, webhook routes и portal identity сконфигурированы; краткая CRM outage даёт degraded по stale threshold, а invalid credential — not-ready. Состояние внешнего nginx/CIDR allow-list проверяется deployment preflight и отдельным synthetic probe, а не этим endpoint.
|
||
- `GET /internal/sync/v1/status`: Bearer service token, internal-only.
|
||
|
||
Status содержит mode, queue depth по состояниям, oldest age, active workflows, commands retry/DLQ, webhook lag, reconciliation cursors, limiter state, last CRM success и settings version. PII, URL credential, Contact/user IDs и raw errors не возвращаются.
|
||
|
||
## 15. Observability
|
||
|
||
Метрики:
|
||
|
||
- queue depth/oldest age и claim duration;
|
||
- workflow/command transitions;
|
||
- CRM batch size, duration и subcommand outcomes;
|
||
- limiter tokens/throttle/rate-limit errors;
|
||
- retry/DLQ counts и age;
|
||
- webhook accepted/rejected/coalesced/lag по receiver и safe reason, включая отклонение source IP без IP label;
|
||
- reconciliation scanned/updated/recovered-without-webhook/cursor lag и alert о всплеске восстановлений;
|
||
- business alerts open/SLA overdue;
|
||
- mapping invariant violations;
|
||
- readiness and worker heartbeat.
|
||
|
||
Labels только low-cardinality: command type, outcome, safe error code, event type. `user_id`, Contact ID, phone, email и fingerprint не labels.
|
||
|
||
JSON logs содержат event name, service/version/environment, request/trace/span/correlation/workflow/command IDs, safe status/error code. PII маскируется минимум до первых двух и последних двух читаемых символов; предпочтительно не логируется вообще. Secrets, URL webhook, request/response body и SQL parameters запрещены.
|
||
|
||
## 16. Security
|
||
|
||
- approved Bitrix portal — единственный CRM egress `:443`;
|
||
- TLS verify обязателен;
|
||
- egress redirect выключен либо повторно проверяет host allow-list;
|
||
- container non-root, read-only rootfs, dropped capabilities;
|
||
- public webhook проходит source IP CIDR allow-list и rate/body/method limits nginx ВМ2;
|
||
- internal status отсутствует в public server block nginx ВМ2;
|
||
- runtime secret files read-only и service-specific;
|
||
- constant-time token compare;
|
||
- query string и form body receiver routes исключены из всех журналов и traces;
|
||
- JSON/form parsing bounded;
|
||
- DB payloads не содержат PII snapshots без необходимости;
|
||
- зависимые образы/dependencies pinned и сканируются;
|
||
- test/prod — разные credentials, secrets, DB/settings и deployments.
|
||
|
||
Разделение тестовых и production Contact на одном портале является внешней организационной ответственностью. Поведение sync-service одинаково; test должен быть prod-like. Это принятое ограничение не отменяет отдельные credentials и endpoint tokens.
|
||
|
||
## 17. Cutover
|
||
|
||
Cutover не управляется постоянным `replay=false` на каждом startup.
|
||
|
||
Одноразовая процедура:
|
||
|
||
1. Остановить старый stub/worker.
|
||
2. Применить migrations и grants.
|
||
3. Зафиксировать `cutover_watermark`.
|
||
4. Перевести все существовавшие до watermark `pending/retry` contact-задачи в `cancelled` с причиной `initial_full_sync_cutover`; слово `full` здесь относится к переходу со stub на full mode, а не к reconciliation.
|
||
5. Не создавать backfill для active пользователей без mapping.
|
||
6. Проверить env/secrets/settings/fields/rights/webhook.
|
||
7. Запустить сервис disabled и выполнить preflight.
|
||
8. Включить обработку только задач после watermark.
|
||
9. Наблюдать queue/webhook/DLQ/CRM limits.
|
||
|
||
Принятое бизнес-ограничение: ранее зарегистрированные пользователи без mapping могут никогда не синхронизироваться, пока новое отслеживаемое событие не создаст задачу. Это не считается дефектом первого релиза.
|
||
|
||
Rollback:
|
||
|
||
- остановить claims;
|
||
- дождаться/ограниченно завершить in-flight;
|
||
- выключить sync без отмены новых pending;
|
||
- не откатывать уже созданные Contact/mapping автоматически;
|
||
- сохранить workflow/audit для последующего controlled resume;
|
||
- DB downgrade не выполняется, если появились production rows нового формата.
|
||
|
||
## 18. Тестовая матрица
|
||
|
||
### Unit
|
||
|
||
- state transitions и запрещённые переходы;
|
||
- trigger decision/no-op/suppress;
|
||
- E.164 validation;
|
||
- branch В0–В2 и deterministic newest;
|
||
- field mapping/citizenship/email;
|
||
- coalesce событий без event ID и дедупликация при наличии стабильного event ID;
|
||
- backoff/jitter/24h horizon;
|
||
- limiter/batch flush;
|
||
- query token/domain/member/document ID validation;
|
||
- PII/secret redaction.
|
||
|
||
### Integration PostgreSQL
|
||
|
||
- concurrent claim через `SKIP LOCKED`;
|
||
- lease expiry и fencing stale worker;
|
||
- trigger + business transaction atomicity;
|
||
- partial unique active mapping;
|
||
- миграция mapping из `han_app` в `bitrix_sync` без двух writable copies;
|
||
- удаление `ClientProfile.bitrix_contact_id` и его partial index после переключения readers;
|
||
- `contact.rebind` при success, partial CRM failure, retry и конфликте целевого Contact;
|
||
- `SET LOCAL han.sync_suppress` без leakage в pool;
|
||
- crash после CRM success до local commit;
|
||
- settings activation/version rollback;
|
||
- cutover watermark cancellation.
|
||
|
||
### Bitrix contract
|
||
|
||
- `crm.duplicate.findbycomm` с `+7XXXXXXXXXX`;
|
||
- `crm.item.list` для `entityTypeId=3` с `>=updatedTime`, `opened=1` и registration field `=1`;
|
||
- minimal select;
|
||
- add/get/update Contact;
|
||
- custom fields read/write;
|
||
- citizenship dictionary;
|
||
- batch mixed success/failure;
|
||
- `QUERY_LIMIT_EXCEEDED` и `OPERATION_TIME_LIMIT`;
|
||
- smart-process create/get/update;
|
||
- Contact/alert HTTP-webhook robot form-urlencoded payload;
|
||
- missing/invalid/rotated receiver query token;
|
||
- совпадение query ID с `document_id`, portal domain/member ID и alert entity type.
|
||
|
||
### Failure
|
||
|
||
- timeout до/после отправки create;
|
||
- service restart на каждом workflow step;
|
||
- PostgreSQL/Bitrix/OTEL outage and recovery;
|
||
- lost/duplicate/out-of-order webhook;
|
||
- новый легитимный source IP вне allow-list с восстановлением изменений reconciliation;
|
||
- dead worker and lease recovery;
|
||
- malformed CRM response;
|
||
- credential revoked;
|
||
- mapping invariant violation;
|
||
- graceful SIGTERM с bounded drain.
|
||
|
||
### Load
|
||
|
||
- вход 100 событий/мин при lag не более 30 секунд в healthy CRM;
|
||
- batch utilization;
|
||
- очередь растёт предсказуемо при throttle и восстанавливается после него;
|
||
- reconciliation до 10 000 Contact не нарушает основной SLA;
|
||
- отсутствие DB connection-pool starvation.
|
||
|
||
### Security
|
||
|
||
- least-privilege negative tests;
|
||
- public receiver source IP allow-list, size/rate/auth;
|
||
- отсутствие query token и form body в nginx/app logs, traces и errors;
|
||
- SSRF/redirect host rejection;
|
||
- secrets absent in env dump/status/logs/traces;
|
||
- PII absent in metric labels и technical DLQ;
|
||
- internal endpoint недоступен через public host ВМ2.
|
||
|
||
## 19. Definition of Done
|
||
|
||
- три автоматических App→CRM сценария и административный `contact.rebind` durable и идемпотентны;
|
||
- Contact HTTP-webhook робот и reconciliation обновляют App без echo;
|
||
- receiver принимает фактический form-urlencoded/query-token контракт, фильтрует source IP и не раскрывает query;
|
||
- canonical mapping и rebind audit находятся только в `bitrix_sync`;
|
||
- create восстанавливается после ambiguous outcome без дубля;
|
||
- business alerts создаются, coalesce и отслеживаются;
|
||
- technical failures идут в SigNoz/DLQ, а не в business process;
|
||
- batch/rate limit/retry соответствуют этой спецификации;
|
||
- secrets доставляются штатным manager и не раскрываются;
|
||
- health/status/metrics отражают реальное состояние;
|
||
- migrations, grants, OpenAPI и runbook синхронизированы;
|
||
- unit/integration/contract/failure/load/security gates пройдены;
|
||
- cutover и rollback отрепетированы;
|
||
- stub claims и `crm_sync_implemented=false` удалены из канонических документов.
|
||
|
||
## 20. Принятые решения и остаточные риски
|
||
|
||
Принято:
|
||
|
||
- один портал и отдельный credential-bearing CRM REST webhook технического пользователя;
|
||
- одна replica с HA-safe leases;
|
||
- default REST rate 2 HTTP requests/sec;
|
||
- два HTTP-webhook робота с отдельными query tokens, source IP allow-list и инкрементальной reconciliation;
|
||
- canonical mapping в `bitrix_sync.entity_external_mapping`;
|
||
- строгий upstream E.164;
|
||
- бизнес-конфликты через smart process, technical alerts через SigNoz;
|
||
- retry horizon 24 часа;
|
||
- отсутствие initial backfill;
|
||
- отсутствие scheduled full reconciliation в первом релизе.
|
||
|
||
Остаточные риски:
|
||
|
||
- организационное разделение test/prod на одном портале не обеспечивается кодом;
|
||
- query token может быть виден администраторам портала и в интерфейсе настройки робота; риск снижается разграничением доступа, IP allow-list и ротацией, но не устраняется;
|
||
- IP-адреса отправителей Битрикс24 могут измениться без предварительного уведомления; до review allow-list webhook будут отклоняться, а задержка восстановления ограничена interval инкрементальной reconciliation;
|
||
- обычный webhook может теряться, поэтому задержка ограничена reconciliation interval;
|
||
- ручная перепривязка остаётся ops-действием, хотя ограничена stored procedure;
|
||
- отсутствие backfill оставляет часть старых пользователей без CRM mapping - принято, т.к. нет реальных пользователей;
|
||
- фактические Bitrix custom field/process IDs появляются только после настройки портала и должны пройти preflight.
|