# Потоки данных и контракты интеграций Статус: предложение; INT 01–04, NFR 01–03. Все частоты ниже — **настраиваемые стартовые рекомендации**, не утверждённый SLA. Сфера/Time только читаются. Реальные endpoint Time и конфигурация 1С неизвестны: в таблицах честно указан блокирующий контракт, а не выдуманные методы. Публичные REST-методы Bitrix24 проверены по документации; доступность полей конкретной коробки Сферы ещё требует read-only контрактной проверки. ## Общий протокол входящих потоков 1. Scheduler или оператор создаёт SyncRun. Уникальный source+scope+scheduledSlot не даёт дубликат задания; worker берёт lease с TTL и heartbeat. run получает фиксированный asOf и snapshot конфигурации. 2. Адаптер читает страницы в staging, следует `next`/cursor конкретного API; не считает короткую страницу концом, если есть next. Сортировка по стабильному ID; cursor и исходные version/hash сохраняются. Bitrix может вернуть ошибку в HTTP 200: проверяется тело `error`. 3. Сетевой timeout: предложение 20 с, не более 5 попыток на run, exponential backoff с jitter 1–60 с и учётом Retry-After. 429/5xx — повтор; 401/403 — остановка до исправления credentials; невалидная схема — quarantine и failed. Повтор/задержка инъецируются для детерминированных тестов. 4. Только полный валидный снимок/страница с подтверждённой границей коммитится с checkpoint в одной транзакции. Для full reconcile удаление отсутствующих допускается только после успешного завершения всей области; обрыв страницы не меняет статус ранее известных записей. Незнакомые ID не сопоставляются по ФИО автоматически. 5. Lease имеет fencing version: потерявший lease worker не может активировать старый snapshot. Повтор обработки — upsert по внешнему ключу и версии/hash. Snapshot активируется атомарно; отчёт не смешивает старые и новые страницы одного периода. 6. При недоступном источнике сохраняется последний целый снимок, lastSuccessAt не меняется, UI показывает stale. Freshness доступа и ставок строже, чем справочных имён (Q-03/Q-08); ошибка синка не даёт дополнительных прав. ## F1. Сотрудники, подразделения, ресурсные роли — INT 02 ```mermaid sequenceDiagram participant W as Sync runner participant B as SphereClient participant S as Сфера participant D as БД W->>B: listDepartments / searchUsers B->>S: department.get / user.get, страницы S-->>B: ID, атрибуты, ACTIVE, next B-->>W: Нормализованные записи и полнота W->>D: staging + проверка mapping ролей W->>D: activate snapshot, мягкая деактивация, отзыв сессий Note over W,D: История назначений не удаляется ``` | Источник → получатель | Сущность и поля / метод | Триггер | Частота | Ключ и идемпотентность | Ошибка / недоступность | Удаление / деактивация | | --- | --- | --- | --- | --- | --- | --- | | Сфера → DirectorySync → Employee | `user.get`: ID, NAME, LAST_NAME, ACTIVE, WORK_POSITION, UF_DEPARTMENT; активные для кандидатов, сохранённые ID отдельно без фильтра активности | Расписание + ручной run | 15 мин; полная сверка известных ID раз в сутки | sphere:user:ID; upsert sourceVersion/hash, имена не ключи | Общий протокол; отсутствующий обязательный ACTIVE/ID — неполный снимок, не default active | Явный ACTIVE=false отключает вход и выбор в новых строках, отзывает сессии; отсутствие ID → missing_pending, запрет новых назначений, отдельная проверка, историю оставить | | Сфера → DirectorySync → Department | `department.get`: ID, NAME, PARENT; `user.get.UF_DEPARTMENT` задаёт связи many-to-many | Расписание + ручной run | 15 мин; полная сверка сутки | sphere:department:ID; уникальные employeeId+departmentId | Цикл дерева/неизвестный parent → quarantine; последний валидный граф видим как stale | Подтверждённо отсутствующее подразделение inactive; связи/история сохраняются; отчёт показывает «Не указано», если основное не выбрано | | Сфера + утверждённое mapping → ResourceRole | WORK_POSITION и/или согласованное UF-поле; `user.fields` для проверки схемы; не существует универсального REST «роль стаффинга» | Изменение профиля или версии mapping, явный пересчёт | При синке 15 мин; mapping по запросу РМ | employeeId+mappingVersion; результат пересчёта не создаёт вторую роль | Неизвестная должность → unmapped и запрет подбора до решения, не автоматическая выдача доступа | Роль не удаляет прошлые назначения; новая роль действует для новых решений; права pm/rm отдельно | STT-215 использует подбор роли по WORK_POSITION как подсказку. Здесь нельзя автоматически переносить его семь ролей в шесть ролей ТЗ. Mapping и многоподраздельность требуют Q-07. Для аккаунта, внезапно исчезнувшего из выборки, доступ временно приостанавливается, но увольнение не фиксируется без подтверждения. Это отличается от необратимого удаления данных. ## F2. Проекты и РП — INT 01 ```mermaid sequenceDiagram participant W as Sync runner participant P as ProjectSource participant B as Сфера participant D as БД W->>P: read(cursor) P->>B: sonet_group.get ИЛИ crm.deal.list ИЛИ crm.item.list B-->>P: ID, название, ответственный, закрытие P-->>W: SourceProject и reference пользователя W->>D: ExternalIdentity + Project + ProjectManager Note over W,D: Нет подтверждённого mapping РП — нет доступа к проекту ``` | Источник → получатель | Сущность и поля / метод | Триггер | Частота | Ключ и идемпотентность | Ошибка / недоступность | Удаление / деактивация | | --- | --- | --- | --- | --- | --- | --- | | Сфера: рабочая группа → ProjectSource → Project | `sonet_group.get`: ID, NAME, OWNER_ID, CLOSED, PROJECT; `sonet_group.user.get`: USER_ID, ROLE для состава. OWNER_ID только кандидат РП, не бизнес-гарантия | Расписание + ручная сверка | 15 мин; полная сутки | sphere:group:ID; membership по projectId+principalId; версия mapping | Если владелец/РП не сопоставлен, quarantine прав; старые имена можно читать, новые заявки блокировать | CLOSED=Y закрывает новые заявки, назначения остаются; отсутствие после полной сверки → missing_pending, не hard-delete | | Сфера: сделка → альтернативный ProjectSource | `crm.deal.list`, `crm.deal.fields`: ID, TITLE, ASSIGNED_BY_ID, STAGE_ID, CLOSED, согласованное UF-поле РП; не любая сделка — проект | Расписание + ручная сверка выбранной в Q-01 области | 15 мин; полная сутки | sphere:deal:ID, sourceVersion/hash | CRM-права могут скрывать сделки: пропуск не означает удаление; failed/stale и сверка с владельцем подключения | Закрытие сделки трактуется по утверждённому mapping статусов проекта; история сохранена | | Сфера: смарт-процесс → альтернативный ProjectSource | `crm.type.list` → entityTypeId; `crm.item.fields` и `crm.item.list`: id, title, assignedById, stageId, согласованные custom fields | Расписание + ручная сверка после настройки типа | 15 мин; полная сутки | sphere:smart:entityTypeId:id; без типа ID неоднозначен | Неизвестные поля/стадии не дают доступ автоматически; схема блокирует активацию снимка | Терминальная стадия из mapping закрывает проект; отсутствие после полного обхода — pending confirmation | Рекомендуем рабочие группы: задачи Time уже связаны со Сферой, `tasks.task.list` возвращает groupId, подход согласуется со STT-215 и не требует CRM-модели для планирования. Сделка уместна, если именно там уже ведутся проекты и РП. Смарт-процесс уместен при действующем реестре проектов с отдельным жизненным циклом; создавать его только ради интеграции не требуется. Q-01 решает сущность, признак проекта, связь РП (один/несколько) и закрытие. Любой вариант нормализуется в один Project; остальной домен не меняется. ## F3. Фактические часы из Time Лоции ```mermaid sequenceDiagram participant W as Sync runner participant T as TimeSource participant B as SphereClient participant D as БД W->>T: read(window, cursor) T-->>W: entryId, userId, taskId, date, seconds, version W->>B: getTasks(неизвестные taskId) B-->>W: taskId и groupId W->>D: сопоставление сотрудника и проекта, quarantine неизвестных W->>D: upsert entries + coverage + checkpoint Note over W,D: Только полное покрытие позволяет вывести явный ноль ``` | Источник → получатель | Сущность и поля / метод | Триггер | Частота | Ключ и идемпотентность | Ошибка / недоступность | Удаление / деактивация | | --- | --- | --- | --- | --- | --- | --- | | Time → TimeSource → ActualEntry/Coverage | Реальный read API/export **не предоставлен, Q-05**. Требуем поля entryId, sphereUserId, sphereTaskId, workDate, durationSeconds, updatedAt/version, deleted, границы полного покрытия; `TimeSource.read` — наш порт, не метод Time | Расписание, ручная сверка периода | Предложение 15 мин текущий и предыдущий месяц; ночная сверка открытых периодов; закрытые по запросу | time:entry:entryId; новая версия заменяет старую, не прибавляется; при экспорте без ID — атомарная замена полного окна, не hash как ID | Нет схемы/права/coverage → unknown/stale; карантин сохраняет исходные строки, частичная сумма не становится полной | Tombstone удаляет вклад записи новой версией; отсутствие считается удалением только в полном снимке окна; старый факт хранится в истории | | Сфера → SphereClient → TaskProjectMapping | `tasks.task.list` с ID-пачками до 50 как STT-215: id, groupId, responsibleId; пользователь факта берётся из Time, не из responsibleId | По новым taskId при импорте; перепроверка mapping | В каждом run для неизвестных; полная по окну сутки | sphere:task:ID → projectId, version/effectiveFrom | Несопоставленный task/group или проект иной сущности → quarantine; ручная подтверждённая связь по ID (Q-01/Q-05) | Удаление задачи не удаляет списанные часы; перенос в группу не переписывает исторический проект без аудируемого решения | Сумма seconds/3600 даёт A; совместимое actualFte = A/H при H>0. Исходные секунды сохраняются, перерасчёт календаря не меняет A. Агрегация employeeId×projectId×month. Нет записи ≠ 0; нули выводятся только когда источник подтверждает полное покрытие соответствующей области. Нельзя обрезать импортированный факт выше 2 H: это конфликт с FR 64, запись уходит в quarantine и Q-05, пока правило не утверждено. Отпуска не извлекаются из нерабочих задач эвристикой. Ручной факт прототипа не суммируется с Time автоматически: предложение — Time имеет приоритет в покрытой области, ручной fallback только вне неё, correction — отдельная аудируемая запись после утверждения Q-05. Часы отпуска пока отдельные локальные записи РМ; подключение отсутствий из 1С требует отдельного подтверждённого контракта (Q-06). ## F4. Календарь, отпуск и расходные ставки — 1С ```mermaid sequenceDiagram participant W as Sync runner participant C as OneCSource participant D as БД participant R as Requests W->>C: calendar(year) и разрешённые costRates C-->>W: Полные версии календаря и ставок W->>D: validate, stage, activate version R->>D: получить серверную ставку и версию календаря R->>D: сохранить costRateSnapshot при решении Note over D,R: Смена справочника не переписывает согласованную ставку ``` | Источник → получатель | Сущность и поля / метод | Триггер | Частота | Ключ и идемпотентность | Ошибка / недоступность | Удаление / деактивация | | --- | --- | --- | --- | --- | --- | --- | | 1С → OneCSource → CalendarVersion/Day | Endpoint **не известен, Q-06**: согласованный HTTP-сервис/выгрузка, не универсальный REST 1С. Поля calendarId, year, date, hours, version, timezone; рабочие выходные и сокращённые дни явные | Расписание + публикация/ручная загрузка версии | Раз в сутки на текущий/следующий год; по запросу | 1c:calendar:calendarId:version:date; версия активируется целиком | Пропуск дня/дубликат/невалидные часы → отказ версии; нет валидного календаря периода → запрет новых расчётных решений, не скрытый fallback | Историческая версия сохраняется; удалённый день не превращается в выходной; смена задним числом через явную ревизию | | 1С либо утверждённый другой источник → CostRateVersion | Endpoint **не известен, Q-04**. Поля externalEmployeeId, validFrom, validTo, costPerHour, currency, version; связь с sphereUserId отдельным проверенным mapping | Расписание + ручной refresh перед решением при stale | Предложение сутки; допустимый возраст — Q-03/Q-04 | employeeId+validFrom+sourceVersion; интервалы не пересекаются | Нет ставки, валюты RUB или mapping → C неизвестна, approve заблокирован; нет выдачи всей зарплатной базы | Закрытие ставки действует на новые решения, costRateSnapshot прошлых назначений остаётся; missing не равно C=0 | | РМ → Reports → VacationHours | До контракта отсутствий: PATCH /api/v1/report-data, employeeId/month/hours 0…H; endpoint 1С для отпусков не предполагается | Явное сохранение РМ | По запросу | employeeId+month+manualRevision, команда идемпотентна | 409 сохраняет форму; незаданный отпуск даёт unknown Nбез | Удаление ручной записи возвращает unknown; увольнение сотрудника не стирает отпуск истории | Сокращённый день требует P=Σfte×hours(day), H=Σhours(day), месячный FTE=P/H; days по-прежнему важнее месячного значения. Это расширение weekday×8 из calc.js и должно быть принято в Q-06. Отпуск вычитается из N один раз. При согласовании берётся C, действующая на дату решения (как текущая ставка прототипа); если нужны помесячные расходы по истории ставок, это отдельное изменение Q-04, не скрытая смена формулы. ## F5. Вход и определение роли — NFR 01 ```mermaid sequenceDiagram participant U as Браузер participant A as Access participant I as Bitrix24 OAuth или SSO participant D as Session и Grants U->>A: начать вход A-->>U: redirect + одноразовый state U->>I: аутентификация I-->>A: callback с code и state A->>I: серверный обмен и проверка личности I-->>A: подтверждённый subject A->>D: найти Principal, active, AccessGrant A-->>U: HttpOnly cookie или отказ доступа U->>A: запрос бизнес API A->>D: проверить сессию, активность и scope ``` | Источник → получатель | Сущность и поля / метод | Триггер | Частота | Ключ и идемпотентность | Ошибка / недоступность | Удаление / деактивация | | --- | --- | --- | --- | --- | --- | --- | | Bitrix24 OAuth → Access → Session | Authorize/token endpoints конкретного портала/приложения подтверждаются Q-08; после обмена `user.current`: ID/ACTIVE. Code flow, state, фиксированный redirectUri; секреты только server | Начало входа и callback | Каждый вход; refresh по expires_in при необходимости | issuer/portal+userId → Principal; state одноразовый, повтор callback не создаёт чужую сессию | Неверный state/issuer/redirect → отказ; OAuth недоступен → новый вход запрещён; токены/коды не логируются | ACTIVE=false → revoke sessions; локально отозванный grant проверяется на каждом API; скрытая деактивация ограничена freshness Q-08 | | Корпоративный SSO → альтернативный IdentityProvider | OIDC discovery/authorize/token/userinfo по metadata согласованного issuer; sub/iss/aud/exp/nonce, PKCE S256; связи sub→sphereUserId только подтверждённые | Вход, обновление сессии | Каждый вход, сроки Q-08 | issuer+sub; не email, одноразовый state/nonce | Проверка подписи/JWKS/audience/expiry; ошибка → deny, не fallback на самозаявленный ID | Событие revoke при наличии и периодическая сверка; удаление связи закрывает сессии, Employee остаётся | | Серверный grant mapping → Access → scoped API | AccessGrant principalId, role pm/rm, projectIds, viewCost; источник состава РП — утверждённый F2, РМ — назначение уполномоченным владельцем | Каждый API-запрос; смена grants | Без кеша права в первом релизе, источник проверяется по freshness | principalId+grantVersion; клиент не задаёт роль | Нет grant → 403; чужой объект → 404; неизвестная свежесть доступа после согласованного предела → deny | Отзыв применяется к уже открытой сессии на следующем запросе, история автора сохранена | Сессия — случайный opaque ID в Secure/HttpOnly/SameSite=Lax cookie, запись/expiry на сервере; state для OAuth хранится отдельно с коротким TTL. POST/PATCH требуют CSRF token и проверку Origin. Нельзя считать локальное приложение Bitrix с произвольным DOMAIN в callback доверенным источником: issuer/домен жёстко allowlisted. В варианте Bitrix поддержка PKCE проверяется на портале; до подтверждения не объявляется готовой. Служебный webhook для синка не идентифицирует посетителя. ## F6. Заявки, назначения и мастер-данные — INT 03, NFR 02 ```mermaid sequenceDiagram participant U as РП или РМ participant A as Requests participant P as Assignments participant D as БД U->>A: commandId, expectedVersion, действие A->>A: доступ, валидация, idempotency A->>D: BEGIN и блокировки сотрудников A->>D: перечитать заявку, версии, ставки A->>P: applyDecision в той же Tx P->>P: весь дневной план, лимит FTE P->>D: новая версия назначения A->>D: DecisionLog + Request + CommandReceipt A->>D: COMMIT A-->>U: результат или типизированный конфликт Note over A,D: Любая ошибка до COMMIT — полный ROLLBACK ``` | Источник → получатель | Сущность и поля / метод | Триггер | Частота | Ключ и идемпотентность | Ошибка / недоступность | Удаление / деактивация | | --- | --- | --- | --- | --- | --- | --- | | РП/РМ → Requests → локальная БД | `/api/v1/requests`, `/decisions`, `/assignments/:id/changes`; Request/ResourceLine/Assignment/Change/DecisionLog; методы и поля — architecture.md | Явное действие с серверной авторизацией | По запросу | commandId+principalId и hash; версии агрегатов, уникальное pending change | Конфликт/недоступная БД → ошибка без частичной записи; форма сохранена, повтор безопасен | Физического удаления решения нет; вывод — новая версия дневного плана, сотрудник/проект inactive сохраняет историю | | Сервис → Сфера | **Нет потока записи в рекомендуемом варианте**; INT 03 ожидает Q-02 | Не запускается до решения заказчика | Отключён | Локальные ID — мастер; внешние ID только справочников | Недоступная Сфера не откатывает уже совершённое локальное решение; stale-политика для новых действий отдельно | Никакие delete/update Bitrix не вызываются | Если заказчик выбирает Bitrix мастер: заменить порт хранения Requests/Assignments, заново спроектировать атомарность решений и журнал/компенсации, пересмотреть ADR-0001 и приёмку. Это существенная развилка, не «флажок». Если нужна только публикация копии решения — отдельный outbox после локального commit, ключ eventId/version, retry и dead-letter; поле локального состояния не объявлять синхронизированным до подтверждения. В текущие пакеты запись обратно не включена. ## Достоверность API и источники Проверено 13.10.2026 по первичной документации: [рабочие группы и поля](https://apidocs.bitrix24.ru/api-reference/sonet-group/sonet-group-get), [пользователь текущего токена](https://apidocs.bitrix24.com/api-reference/user/user-current.html), [ограничения user scope](https://apidocs.bitrix24.com/api-reference/user/user-scope.html), [department API и REST 3.0](https://apidocs.bitrix24.com/api-reference/departments/index.html), [crm.item.list](https://apidocs.bitrix24.com/api-reference/crm/universal/crm-item-list.html), [поля CRM](https://apidocs.bitrix24.com/api-reference/crm/universal/object-fields.html). REST v2 department.get выбран для совместимости STT-215; новые humanresources методы не обязательны для этой архитектуры. Возможность вызова конкретного метода не доказывает наличие прав на все записи. Перед живым импортом L3 smoke на тестовом портале проверяет scope, типы ID/ACTIVE, next, доступ к неактивным, группе/проекту и полям. Боевую Сферу в этой задаче не вызывали; тестовый STT-27 не нужен для документирования неподтверждённых контрактов.