Files
han-app/functional_blocks (business logic)/chat-requirements.md
T

619 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Бизнес-постановка: Обмен сообщениями (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 п.2324; **не** путать с бейджем уведомлений.
- 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=<cursor>` (и 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`.