# Бизнес-постановка: Обмен сообщениями (Chat / Dialog) **Статус:** v1 — консолидация принятых решений из `HAN_chat_specification` (`arch-00`…`arch-05`, `module-01`, `module-05`, `module-06`); открытые вопросы зафиксированы в §17 **Продукт:** HAN Chat (клиентское приложение + `api-backend` + `message-safety` + `bitrix-local-app` / Open Lines) **Источники:** архитектура `HAN_chat_specification`; макет Figma (**не канон** — только визуализация; при расхождении приоритет у этого ТЗ и arch-документов) **Связанный backlog:** непрочитанные сообщения на кнопке «Чат» (синхронизация между устройствами); UX ошибки отложенного сообщения после входа **Смежно:** пользователь (auth/bootstrap), уведомления (`send_chat_message`, кнопки Чат/Оператор), популярные вопросы, Message Safety **Нормативная часть — §1–§16.** §17 — ненормативный журнал решений и открытых вопросов; при расхождении с §1–§16 приоритет у §1–§16. При расхождении этого документа с arch/module после их обновления — приоритет у arch/module до синхронизации. --- ## 1. Цель Дать клиенту канал диалога с оператором компании: отправка текста и файлов из приложения в Bitrix24 Open Lines и получение ответов оператора в реальном времени — с проверкой исходящих сообщений (Message Safety), идемпотентностью, ownership и деградацией без потери уже принятых данных. Чат — канал **«клиент ↔ оператор»**. Уведомления — канал **«компания → клиент»** вне чата (см. `notification-requirements.md`). Непрочитанные ответы оператора **не** моделируются как уведомления; индикатор непрочитанного чата — отдельная фича (§3.2, §17.2). --- ## 2. Два направления обмена | Направление | `sender_type` | Кто инициирует | Проверка Message Safety | Доставка | |---|---|---|---|---| | **C→O. Клиент → оператор** | `client` | Frontend (JWT) | **Обязательна** до Open Lines | `api-backend` → `bitrix-local-app` → Open Lines | | **O→C. Оператор → клиент** | `company` | Bitrix24 webhook → `bitrix-local-app` → inbox API | **Нет** outbound moderation (доверенный канал); MIME/size/antivirus policy модуля | App DB + WS / polling | Правила: 1. Чат доступен **только авторизованному** клиенту (JWT + `bootstrap`). Гость инициирует auth; текст сохраняется локально и отправляется после входа (§6.4). 2. У пользователя **не более одного активного** диалога (`open` \| `waiting_for_company` \| `waiting_for_client`). 3. `dialog_id` приложения **равен** `external_chat_id` для Open Lines. 4. MVP: одно исходящее сообщение — либо текст, либо ровно один файл (`content_kind`), не оба сразу. 5. Клиент на `POST .../messages` получает **только финальный** результат (или ошибку инфраструктуры), не промежуточное «обрабатывается». 6. Источник истины ленты — App DB; WS — at-most-once best effort; после reconnect — REST reconcile. --- ## 3. Границы релиза ### 3.1. В scope - Один активный диалог на пользователя; ленивое создание перед первым сообщением. - Исходящие: `content_kind` `text` \| `file`; входящие: текст и файлы оператора. - Orchestration Message Safety: sync check + sync-wait poll `task_id` + checkpoint `safety_tasks` + recovery. - Presigned upload клиента в S3-quarantine → complete → safety → promote в S3-data attachments. - Durable outbox App → Open Lines; idempotent delivery по `message_id`. - Inbox Open Lines → App: `message.new`, `dialog.closed`; idempotency по `(external_chat_id, bitrix_message_id)` / `event_id`. - Realtime `WS /api/v1/realtime` + polling fallback истории сообщений. - Популярные вопросы как обычная отправка текста после auth. - Общий механизм **отложенного сообщения** на frontend (ручной ввод, популярный вопрос, CTA уведомлений `send_chat_message`). - Кнопки UI «Чат» и «Оператор» (`tel:` ← `operator.call.phone`) — смежно с уведомлениями. - Нормализация текста входящих из Bitrix (удаление служебной разметки отправителя) — уже закрытый дефект backlog. - Ownership, rate limits, `Idempotency-Key` (TTL 24 ч) на create dialog / send message. - Константы `chat.attachments.*`, `rate_limit.message_send.*`, `operator.call.phone`. ### 3.2. Вне scope - Смешанное сообщение «текст + файл(ы)» (post-MVP, отдельная версия API). - Несколько вложений в одном исходящем сообщении. - UI-раздел «История чатов / список диалогов» как продуктовый экран — **deprecated** для MVP frontend (один чат с компанией); backend `GET /api/v1/dialogs` **не удаляется**. - Индикатор непрочитанных сообщений чата и sync между устройствами (`Dialog.client_last_opened_at` и аналоги) — backlog п.23–24; **не** путать с бейджем уведомлений. - Typing indicators, реакции, редактирование/удаление сообщений клиентом, ответы на конкретное сообщение (quote/reply). - Голосовые / видеосообщения, стикеры, произвольные форматы сверх `chat.attachments.*`. - Push / deep link в чат (модель может быть push-ready позже). - Админ-модерация очереди `needs_review` (enum зарезервирован, в MVP не создаётся). - CRM Contact sync в hot path чата (`bitrix-sync` **не** участвует в Open Lines delivery). - Реальная антивирус/LLM-модерация: в MVP допускается stub `message-safety` с фиксированными правилами теста; канонический контракт вердиктов — arch-02 (`200/203/403`). --- ## 4. UI | Место | Поведение | |---|---| | Главная: поле ввода | Отправка текста; без auth → согласия → OTP → bootstrap; затем немедленный переход в чат и отложенная отправка | | Главная: популярные вопросы | Тап = автоотправка текста вопроса (тот же поток, что ручной ввод) | | Кнопка «Чат» | Открывает текущий активный диалог или создаёт его при отсутствии | | Кнопка «Оператор» | `tel:` на `operator.call.phone` (не чат-сообщение) | | Экран чата | Лента сообщений клиента и компании; статусы доставки по DTO | | Вложения | Выбор файла → init → PUT в quarantine → complete → send `content_kind=file` | | Гость в Центре уведомлений / чате | Auth-gate; после входа — ЛК | Визуал — по Figma. Figma не канон статусов доставки и состава API. --- ## 5. Бизнес-модель ### 5.1. Сущности | Сущность | Схема | Назначение | |---|---|---| | `Dialog` | `han_app` | Диалог клиента с Open Lines | | `Message` | `han_app` | Сообщение в диалоге | | `MessageAttachment` | `han_app` | Вложение (client upload или company inbound) | | `safety_tasks` | `han_app` | Checkpoint sync-wait Message Safety (техническая) | | `delivery_outbox` | `han_app` | Durable намерение доставки в Open Lines | | `openlines_inbox_receipts` | `han_app` | Idempotency приёма событий local app | | `dialog_sessions` | `bitrix_local` | Маппинг чата у `bitrix-local-app` | | `popular_questions` | `han_app` | Справочник текстов быстрых вопросов (не сообщения) | ### 5.2. `Dialog.status` | Значение | Смысл | |---|---| | `open` | Диалог создан, сообщений ещё нет | | `waiting_for_company` | Последнее значимое — исходящее от клиента; ждём оператора | | `waiting_for_client` | Последнее значимое — входящее от оператора; ждём клиента | | `closed` | Закрыт в Open Lines (`dialog.closed` / `ONIMCONNECTORDIALOGFINISH`) | Переходы: | Событие | Новый статус | |---|---| | `POST /dialogs` (новый) | `open` | | Успешная доставка исходящего клиента в Open Lines | `waiting_for_company` | | Сохранено входящее от оператора | `waiting_for_client` | | Inbox `dialog.closed` | `closed` | Из `closed` обратный переход **запрещён**. Новый активный диалог после закрытия — снова через `POST /dialogs`, когда продукт это разрешит; инвариант «один active» сохраняется. ### 5.3. Сообщение: вид и статусы **`content_kind` (исходящее MVP):** | `content_kind` | Тело запроса | `Message.text` | Вложения | |---|---|---|---| | `text` | непустой `text` | текст | 0 | | `file` | `attachment_id` + `checksum` | пустая строка | ровно 1 | Запрещено: текст + файл → `400 mixed_content_not_allowed`; пустое → `400 empty_message`; >1 вложение → `400 too_many_attachments`. **`sender_type`:** `client` \| `company`. **`safety_status`:** | Значение | Смысл | |---|---| | `pending` | Проверка не завершена | | `allowed` | Финальный allow | | `blocked` | Финальный deny | | `needs_review` | Зарезервирован, в MVP не создаётся | Входящие `company` всегда `safety_status=allowed`. **`delivery_status`:** | Значение | Смысл | Клиенту как финал `POST .../messages`? | |---|---|---| | `accepted` | Принято API, safety/доставка ещё не финализированы | нет (промежуточный) | | `processing` | Sync-wait safety | нет | | `delivered` | Allow + ушло в Open Lines (или входящее сохранено) | да | | `rejected` | Deny Message Safety | да (`422 message_blocked`) | | `failed` | Инфраструктурная ошибка (Bitrix/S3/timeout), не safety-deny | да (`503`/`504`) | Инварианты: `rejected` ↔ `blocked`; `delivered` → `allowed`. ### 5.4. Вложение | `scan_status` | Смысл | |---|---| | `pending` | В quarantine, проверка не завершена | | `clean` | Allow, файл в S3-data (после promote) | | `infected` | Deny | | `failed` | Ошибка инфраструктуры проверки | `direction`: `client_upload` \| `company_inbound`. Клиентский файл до allow живёт **только** в S3-quarantine. Постоянных access keys у клиента нет — только короткий presigned PUT/GET. ### 5.5. Идентификаторы | Имя | Назначение | |---|---| | `dialog_id` | UUID диалога = `external_chat_id` Open Lines | | `message_id` | UUID сообщения; ключ idempotent delivery | | `attachment_id` | UUID вложения | | `bitrix_message_id` | ID сообщения в Bitrix для inbox dedup | | `task_id` | Async-проверка Message Safety | | `Idempotency-Key` | Клиентский ключ create dialog / send (Redis + durable fallback), TTL 24 ч | ### 5.6. Константы (`app_settings`) | Ключ | Смысл | Seed | |---|---|---| | `chat.message.max_length` | Макс. длина нормализованного текста; public config для счётчика `n/max` | `4000` | | `chat.attachments.allowed_extensions` | Разрешённые расширения | jpg,jpeg,png,webp,heic,heif,pdf | | `chat.attachments.allowed_mime_types` | Разрешённые MIME | image/jpeg, image/png, …, application/pdf | | `chat.attachments.disallowed_extensions` | Явный deny-list | svg,doc,docx,xls,xlsx,csv | | `chat.attachments.max_size_mb` | Макс. размер | `5` | | `chat.attachments.storage` | Провайдер | `selectel_s3` | | `chat.attachments.upload_mode` | Режим | `presigned_put` | | `chat.attachments.safety_scan_required` | Safety обязателен | `true` | | `chat.attachments.presigned_upload_ttl_seconds` | TTL upload URL | `600` | | `rate_limit.message_send.per_user` | Лимит отправок | `30/minute` | | `rate_limit.message_send.per_dialog` | Лимит на диалог | `20/minute` | | `rate_limit.download_url.per_user` | Лимит download-url | `60/hour` | | `operator.call.phone` | Телефон кнопки «Оператор» | `+74999591007` | Те же `chat.attachments.*` переиспользуются уведомлениями для документов клиента. ### 5.7. Популярные вопросы - Справочник `popular_questions` отдаётся public content API. - Отдельного backend-flow нет: после auth текст уходит как обычное `content_kind=text`. - Без auth — тот же механизм отложенного сообщения, что у ручного ввода. --- ## 6. Поведение UI и сценарии ### 6.1. Создание / открытие диалога 1. Frontend перед первым `POST .../messages` вызывает `POST /api/v1/dialogs` с `Idempotency-Key`. 2. Нет active → `201`, `status=open`. 3. Есть active → `200`, возвращается существующий (новый не создаётся). 4. Кнопка «Чат» открывает этот диалог (или создаёт при отсутствии). ### 6.2. Исходящее текстовое сообщение 1. `POST /dialogs` (если нет `dialog_id`). 2. `POST .../messages` с `content_kind=text`, `Idempotency-Key`. 3. Rate limits (nginx + app). 4. Message Safety (текст, ссылки). 5. Allow → outbox → Open Lines → `delivery_status=delivered`, `Dialog` → `waiting_for_company`. 6. Deny → `422 message_blocked`, в Open Lines **не** уходит; backend сохраняет в истории отдельную `company`-реплику с бизнес-текстом для сообщения или документа. 7. Dependency failure → `503`/`504`, при уже созданном Message — `delivery_status=failed`. ### 6.3. Исходящий файл 1. `POST .../attachments/init` → presigned PUT в **S3-quarantine**. 2. Frontend грузит байты **напрямую в S3** (не через api-backend). 3. `POST .../attachments/{id}/complete` + checksum → HeadObject, metadata, `scan_status=pending`. 4. `POST .../messages` с `content_kind=file`, `attachment_id`, `checksum`. 5. Safety (файл); при `203 pending` api-backend sync-poll `task_id` внутри того же HTTP-запроса клиента. 6. Allow → promote quarantine → S3-data attachments → delivery Open Lines (`message.files` signed URL, `message.text` пустой). 7. Deny → quarantine delete, `blocked`/`rejected`. Пока идёт poll safety, **это** клиентское соединение ждёт; параллельные запросы других клиентов не блокируются. ### 6.4. Отложенное сообщение (гость → после auth) Общий frontend-механизм для: - ручного ввода; - популярного вопроса; - CTA уведомлений с `send_chat_message` (после входа в контуре G). Порядок: 1. Клиент инициирует отправку без JWT → согласия → OTP → `bootstrap` → `session-start`. 2. Экран авторизации **завершается после успешного bootstrap**, даже если последующая отправка в чат упадёт. 3. Затем `POST /dialogs` → `POST .../messages` с сохранённым текстом. 4. Ошибка Bitrix/safety показывается **в контексте чата**, не как «не удалось завершить вход» (backlog п.21). ### 6.5. Входящее от оператора 1. Bitrix24 `ONIMCONNECTOR*` → `bitrix-local-app` (inbox, retry, DLQ). 2. Forward в `POST /internal/openlines/v1/inbox` (`message.new`). 3. api-backend: ownership по `external_chat_id`, save Message `company`/`allowed`/`delivered`, файлы → S3-data + `MessageAttachment`. 4. Текст очищается от служебной разметки отправителя Bitrix (BBCode-префиксы имени и т.п.) — клиент видит чистый текст ответа. 5. `Dialog.status` → `waiting_for_client`. 6. Publish WS `message.new`; при недоступности WS — клиент подтянет через polling. 7. Local app ack delivery в Bitrix после успешного apply / duplicate-ack. Лимиты MIME/size для файлов оператора в MVP — те же `chat.attachments.*`. ### 6.6. Закрытие диалога Inbox `dialog.closed` → `Dialog.status=closed`. Frontend получает `dialog.status` по WS или при следующем GET. Новый активный — только новым `POST /dialogs`. ### 6.7. Realtime и fallback 1. `WS /api/v1/realtime` + JWT. 2. После `connected` — `subscribe` с `dialog_ids` (и опционально `notifications`). 3. События чата: `message.new`, `message.status`, `dialog.status`. 4. Reconnect: backoff 1s…30s; повтор `subscribe`. 5. WS недоступен > 30s → polling `GET .../messages?after=` (и notifications counter при подписке). 6. Ping/pong ~30s. ### 6.8. Скачивание вложения `GET .../attachments/{id}/download-url` → короткий presigned GET + audit `attachment.download_url_issued`. URL в логи/audit не пишется. Ownership обязателен. --- ## 7. Матрицы поведения ### 7.1. Исходящее: safety → delivery | Вердикт safety | `safety_status` | `delivery_status` (финал клиенту) | Open Lines | HTTP клиенту | |---|---|---|---|---| | `200 allow` | `allowed` | `delivered` после успешной отправки; иначе `failed` | да (после allow) | `201` или `503`/`504` | | `403 deny` | `blocked` | `rejected` | нет | `422 message_blocked` | | `203 pending` → затем allow/deny | как финал | как финал | только после allow | финальный код после poll | | timeout / circuit open | по политике модуля | `failed` | нет | `503`/`504` | Клиенту **не** отдаётся промежуточный `processing` как успешный ответ `POST .../messages`. ### 7.2. Идемпотентность клиента | Ситуация | Результат | |---|---| | Тот же `Idempotency-Key` + то же тело | Тот же HTTP-ответ, без повторного side-effect | | Тот же ключ + другое тело | `409 idempotency_key_reused` | | Нет ключа на обязательном endpoint | `400 validation_error` | | TTL | 24 часа (Redis); durable fallback в `idempotency_records` | Доставка в Open Lines идемпотентна по `message_id`. Inbox `message.new` — unique `(external_chat_id, bitrix_message_id)`. ### 7.3. Деградация зависимостей | Зависимость | Влияние на чат | |---|---| | Redis DB0 | Write fail-closed; read ограниченно деградирует | | Redis DB1 (realtime) | REST работает; WS/publish деградирует → polling | | `message-safety` | Отправка недоступна; чтение истории работает | | `bitrix-local-app` / Bitrix | После allow сообщение может стать `failed`; recovery по outbox | | S3 | Файловые операции недоступны; текстовый чат продолжает работать | | Open Lines в readiness | Может быть `degraded`; не обязано валить весь API | ### 7.4. Ownership и ошибки доступа | Ситуация | Код | |---|---| | Чужой `dialog_id` / `attachment_id` / `message` | `404` (существование не раскрывается) | | Нет/невалиден JWT | `401` | | Диалог `closed`, политика запрещает send | по контракту модуля (`409`/`422` — уточнить в OpenAPI при публикации) | | Safety deny | `422 message_blocked` | --- ## 8. Идентификация и корреляция - Публичные id — UUID. - `dialog_id` генерирует приложение при create и передаётся в Open Lines как `external_chat_id`. - При первой доставке `bitrix-local-app` создаёт/обновляет `dialog_sessions`. - `X-Request-ID`, `traceparent`, опционально `X-Ux-Session-Id` — для логов/audit; не auth. - Connector Bitrix: `han_mobile_app`, Open Line id по env/глоссарию. --- ## 9. API Общие конвенции — arch-02: `/api/v1/*` JWT, `/api/v1/public/*`, `/internal/{mnemonic}/v1/*`; JSON snake_case; даты RFC 3339 UTC; cursors opaque. ### 9.1. Клиентские (JWT) | Метод и путь | Назначение | |---|---| | `POST /api/v1/dialogs` | Find-or-create active dialog (`Idempotency-Key`) | | `GET /api/v1/dialogs` | Список/история диалогов (API есть; UI MVP — один чат) | | `GET /api/v1/dialogs/{dialog_id}` | Карточка диалога | | `GET /api/v1/dialogs/{dialog_id}/messages?after=&limit=` | История / polling | | `POST /api/v1/dialogs/{dialog_id}/messages` | Отправка (`Idempotency-Key` + safety) | | `POST .../attachments/init` | Presigned PUT quarantine | | `POST .../attachments/{id}/complete` | Подтверждение upload | | `GET .../attachments/{id}/download-url` | Presigned GET + audit | | `WS /api/v1/realtime` | События чата (и опционально уведомлений) | ### 9.2. Public | Метод и путь | Назначение | |---|---| | `GET /api/v1/public/content` | Тексты + `popular_questions` | | `GET /api/v1/public/settings` | В т.ч. публичные флаги/лимиты UI при `is_public` | ### 9.3. Internal | Метод и путь | Кто → кто | Назначение | |---|---|---| | `POST /internal/safety/v1/messages/check` | api-backend → message-safety | Проверка | | `GET /internal/safety/v1/messages/tasks/{task_id}` | api-backend → message-safety | Poll вердикта | | `POST /internal/openlines/v1/messages` | api-backend → bitrix-local-app | Исходящая доставка | | `GET /internal/openlines/v1/dialogs/{external_chat_id}` | api-backend → bitrix-local-app | Reconciliation | | `POST /internal/openlines/v1/inbox` | bitrix-local-app → api-backend | Входящие события | ### 9.4. Ошибки домена чата | Код | HTTP | Когда | |---|---|---| | `validation_error` | 400 | Нет Idempotency-Key, невалидное тело | | `empty_message` | 400 | Нет текста и вложения | | `mixed_content_not_allowed` | 400 | Текст и файл вместе | | `too_many_attachments` | 400 | >1 вложение | | `attachment_not_completed` | 400 | Send до complete | | `attachment_checksum_mismatch` | 400 | Checksum не совпал | | `unauthorized` | 401 | JWT | | `message_blocked` | 422 | Safety deny | | `idempotency_key_reused` | 409 | Ключ с другим телом | | `resource_state_conflict` | 409 | Конфликт состояния (напр. complete с другим checksum) | | `rate_limit_exceeded` | 429 | Лимит | | `dependency_unavailable` | 503 | Circuit / недоступна зависимость | | `dependency_timeout` | 504 | Timeout зависимости | ### 9.5. DTO `MessageResponse` (REST и WS) ```json { "message_id": "uuid", "dialog_id": "uuid", "sender_type": "client", "content_kind": "text", "text": "Здравствуйте", "attachments": [], "safety_status": "allowed", "delivery_status": "delivered", "created_at": "2026-07-09T12:00:00Z" } ``` Сортировка сообщений: `created_at ASC` (append в ленте). Диалоги: `updated_at DESC`. --- ## 10. Модель данных (схема `han_app`) Общие правила — module-01 §9.1 / arch-05. ### 10.1. `dialogs` | Поле | Описание | |---|---| | `id` | = `dialog_id` = `external_chat_id` | | `user_id` | FK владельца | | `status` | enum §5.2 | | `last_message_at` | | | `closed_at` | при `closed` | | common fields | обязательны | Partial unique: один active dialog на `user_id`. ### 10.2. `messages` | Поле | Описание | |---|---| | `id` | `message_id` | | `dialog_id` | FK | | `sender_type` | `client` \| `company` | | `content_kind` | `text` \| `file` | | `text` | непустой для text; `''` для file | | `safety_status` / `delivery_status` | §5.3 | | `external_message_id` | Bitrix id для inbound | | `client_idempotency_key` | опционально/связка с Idempotency-Key | | `occurred_at` | | | common fields | | Индексы: лента `(dialog_id, created_at, id)`; recovery по `delivery_status`; unique inbound `(dialog_id, external_message_id)`. **Решение:** исходящее создаётся до завершения safety со статусами `pending`/`accepted`, чтобы `safety_tasks` имел FK (crash checkpoint). ### 10.3. `message_attachments` Поля: `id`, `dialog_id`, `message_id NULL` до привязки, `owner_user_id`, `direction`, имена/MIME/size/checksum, `scan_status`, storage keys (working + quarantine), timestamps, common fields. MVP: unique partial — не более одного active attachment на `message_id`. ### 10.4. Технические таблицы | Таблица | Назначение | Физическая очистка | |---|---|---| | `safety_tasks` | Poll/recovery Message Safety | да, по retention | | `delivery_outbox` | App → Open Lines | по статусам/retention модуля | | `openlines_inbox_receipts` | Dedup inbox | по политике модуля | | `idempotency_records` | Durable fallback Idempotency-Key | по `expires_at` | ### 10.5. Схема ключей S3 (чат) ```text quarantine/... # клиентский upload до вердикта attachments/... # проверенные вложения чата (client + company) ``` Бакет documents (`han-chat-documents`) — для документов профиля/уведомлений, **не** hot path чата Open Lines. --- ## 11. Фоновые процессы | Процесс | Владелец | Назначение | |---|---|---| | Safety recovery worker | api-backend | Доводит `203 pending` после обрыва клиентского HTTP | | Delivery outbox worker | api-backend | Retry доставки в local app / Open Lines | | Quarantine orphan cleanup | api-backend / ops | Удаляет просроченные объекты без active task/attachment | | Inbox retry / DLQ | bitrix-local-app | Надёжность webhook → API | | Realtime publish | api-backend + Redis DB1 | Fan-out WS; при сбое — polling | Application-код **не** обходит outbox «в обход» для повторной доставки без idempotency. --- ## 12. Audit и observability | `event_type` | Когда | |---|---| | `dialog.created` | Новый диалог | | `message.submitted` | Принято к обработке | | `message.blocked` | Safety deny | | `message.delivered` | Успех Open Lines / inbound saved | | `message.failed` | Инфраструктурный fail | | `attachment.upload_initialized` / `completed` / `promoted` / `rejected` | Lifecycle файла | | `attachment.download_url_issued` | Presigned GET | | `openlines.inbox_applied` | Входящее применено | Метрики: latency send, safety poll duration/timeout, outbox depth/age/DLQ, inbox duplicate/apply, WS reconnect/drop, rate limit rejects. **Нельзя** использовать `user_id`/`dialog_id` как metric labels. В логах нет: тел сообщений, tokens, presigned URL, Bitrix download URL, полного PII. --- ## 13. Хранение - `Dialog` / `Message` / `MessageAttachment` — прикладные строки, soft-delete, физическое удаление запрещено. - Закрытые диалоги и их сообщения остаются в БД (история API); UI MVP показывает текущий чат. - Quarantine и технические checkpoint — исключения с retention. - S3-data attachments живут с записью вложения; lifecycle бакетов — ops-политика. --- ## 14. Смежные сервисы | Компонент | Роль в чате | |---|---| | **Frontend** | UI чата, отложенное сообщение, upload, WS/polling, кнопки Чат/Оператор | | **api-backend** | Dialogs/messages/attachments, safety orchestration, outbox, inbox apply, realtime | | **message-safety** | Вердикт allow/deny/pending по исходящим | | **bitrix-local-app** | Connector Open Lines, исходящие/входящие, `dialog_sessions` | | **Bitrix24 Open Lines** | Рабочее место оператора | | **S3** | Quarantine + attachments | | **Redis** | Rate limit, idempotency cache, realtime coordination | | **nginx** | `/api/*` + WS upgrade; `proxy_read_timeout` ≥ safety poll budget + запас | | **bitrix-sync** | **Не** в hot path чата | Порядок работ (если поднимать домен с нуля): (1) dialogs + messages text path + idempotency → (2) safety orchestration → (3) Open Lines out + inbox → (4) attachments → (5) WS + polling → (6) recovery/outbox hardening. --- ## 15. Влияние на arch-документы | Документ | Содержание по чату | |---|---| | `arch-00-glossary.md` | `Dialog`, `Message`, статусы, Open Lines terms | | `arch-01-system-architecture.md` | Потоки C→O и O→C, создание диалога | | `arch-02-api-contracts.md` | REST/WS/safety/openlines контракты | | `arch-03-docker-compose-blueprint.md` | Сервисы, WS location, timeouts | | `arch-04-settings-and-content.md` | `chat.attachments.*`, rate limits, operator phone | | `module-01-api-backend.md` | Алгоритмы, таблицы, workers | | `module-05-message-safety.md` | Stub/production safety | | `module-06-bitrix-local-app.md` | Connector / inbox / out | Этот документ собирает бизнес-смысл обмена сообщениями для аналитики и смежных фич; детальные алгоритмы — в module-спеках. --- ## 16. Критерии приёмки 1. Без JWT отправить сообщение нельзя; после auth отложенный текст/популярный вопрос уходит штатным `POST /dialogs` → `POST .../messages`. 2. Не более одного active dialog; повторный `POST /dialogs` возвращает существующий. 3. Text и file взаимоисключающи; mixed/empty/too many → соответствующие `400`. 4. `POST .../messages` возвращает только финальный статус; deny → `422 message_blocked` без доставки в Bitrix. 5. Allow → сообщение видно оператору в Open Lines; `Dialog` → `waiting_for_company`. 6. Ответ оператора появляется в клиенте через WS или polling; `Dialog` → `waiting_for_client`; текст без служебной разметки имени из Bitrix. 7. `dialog.closed` закрывает диалог; из `closed` нельзя вернуться тем же id. 8. Файлы: quarantine → safety → promote; deny чистит quarantine; download только presigned + audit. 9. Идемпотентность create/send соблюдается 24 ч; повтор delivery/inbox не плодит дубли. 10. При падении WS > 30s клиент уходит в polling и не теряет сообщения, уже лежащие в App DB. 11. Недоступность safety блокирует send, но не чтение истории; недоступность S3 не ломает text-only чат. 12. Ошибка отправки после успешного bootstrap не выглядит как ошибка входа. 13. Кнопка «Оператор» берёт номер только из `operator.call.phone`. 14. Популярный вопрос не имеет отдельного API — только text message. 15. Ownership: чужие dialog/attachment → `404`. --- ## 17. Журнал решений и открытых вопросов (ненормативно) ### 17.1. Принятые решения | # | Решение | Раздел / источник | |---|---|---| | D1 | Чат ≠ уведомления; непрочитанный чат не есть Notification | §1, notification-requirements | | D2 | Один active dialog; `dialog_id` = `external_chat_id` | §2, arch-01 | | D3 | MVP content: text XOR file | §5.3, arch-02 | | D4 | Клиент ждёт финальный вердикт на одном HTTP; poll safety внутри api-backend | §6.2–§6.3 | | D5 | Исходящие проходят Message Safety; входящие оператора — доверенный канал | §2, arch-02 | | D6 | Durable outbox + idempotent delivery; inbox dedup | §6, module-01 | | D7 | WS обязателен как primary realtime; polling — fallback | §6.7 | | D8 | UI истории диалогов deprecated; API списка сохраняется | §3.2 | | D9 | Отложенное сообщение — общий frontend-механизм | §6.4 | | D10 | Популярный вопрос = обычный text send | §5.7 | | D11 | Presigned upload напрямую в S3; api-backend не проксирует байты | §6.3 | | D12 | `chat.attachments.*` — единые лимиты для чата (и reuse уведомлениями) | §5.6 | ### 17.2. Открытые вопросы | # | Вопрос | Предложение | Влияние | |---|---|---|---| | Q1 | Индикатор непрочитанных сообщений чата + sync между устройствами (backlog 23–24) | Поле вроде `Dialog.client_last_opened_at` / `last_read_message_id` + WS/REST counter; **не** тип уведомления `message` | Новая мини-постановка | | Q2 | Точный HTTP-код send в уже `closed` dialog | Зафиксировать в OpenAPI (`409 resource_state_conflict` или `422`) | Клиентский UX | | Q3 | Политика показа blocked-сообщения в ленте (полный текст vs redacted) | Минимизация PII в хранении blocked (M8 module-01) + нейтральный UI | DB + frontend | | Q4 | Создание нового dialog сразу после `closed` — всегда разрешено или по бизнес-правилу «сессия поддержки» | MVP: разрешить, пока соблюдён unique active | Продукт / поддержка | | Q5 | Stub `message-safety` с `400` на GET task vs канон arch-02 `403` | Для prod — только канон `200/203/403`; stub не расширяет публичный контракт | module-05 / contract tests | | Q6 | Смешанный content text+files | Отдельный API version post-MVP | arch-02 breaking | | Q7 | Unread badge на кнопке «Чат» vs бейдж колокольчика уведомлений | Развести визуально и в данных | UI + Q1 | --- ## 18. Связь со смежными доменами | Домен | Связь | |---|---| | **Пользователь** | Чат только после JWT + bootstrap; отложенное сообщение после входа | | **Уведомления** | CTA `send_chat_message` пишет в чат; кнопки Чат/Оператор на главной; тип `message` в уведомлениях **запрещён** | | **Документы профиля** | Другой бакет/реестр; не заменяют вложения чата | | **CRM (`bitrix-sync`)** | Contact map по телефону параллельно; не доставляет сообщения Open Lines | Владелец ленты и статусов доставки — `api-backend` + App DB; рабочее место оператора — Bitrix24 Open Lines через `bitrix-local-app`.