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

639 lines
42 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 | **Нет** Message Safety/AV; только MIME/size, residual risk принят | 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).
- До production cutover допускается только явно маркированный stub v1; target Message Safety v2 имеет `200/202/403`, local-only URL checks и file scan.
---
## 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) |
| `bypassed` | Forced allow в emergency MOCK; файл перенесён, но не проверялся |
| `infected` | Deny |
| `failed` | Ошибка инфраструктуры проверки |
`direction`: `client_upload` \| `company_inbound`.
Клиентский файл до allow живёт **только** в versioned S3-quarantine. Presigned PUT подписывает `If-None-Match: *` и checksum; один object key нельзя перезаписать. Постоянных access keys у клиента нет.
### 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 в versioned **S3-quarantine** со signed `If-None-Match: *`, checksum и `Content-Type`.
2. Frontend грузит байты напрямую; повторная запись key получает `412`.
3. `POST .../attachments/{id}/complete` + checksum → фиксация authoritative `version_id + ETag + checksum`, `scan_status=pending`.
4. `POST .../messages` с `content_kind=file`, `attachment_id`, `checksum`.
5. Safety (файл); при `202 pending` api-backend sync-poll `Location` внутри того же HTTP-запроса клиента.
6. Allow → conditional promote сохранённой S3 version (source ETag/checksum match) → S3-data attachments → delivery Open Lines.
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.*`.
Файл оператора считается данными доверенного Bitrix24-channel: он не загружается в quarantine и не проходит Message Safety/ClamAV. Выполняются только MIME/size validation, безопасная выдача download response и audit. Остаточный malware-риск для MVP принят явно.
### 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` |
| `202 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/v2/messages/check` | api-backend → message-safety | Проверка |
| `GET /internal/safety/v2/messages/tasks/{task_id}` | api-backend → message-safety | Poll вердикта; pending = `202` |
| `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 |
| `safety_processing_mode` | `standard | mock`, internal/audit only |
| `safety_config_version` | версия active Message Safety config, internal/audit only |
| `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, `quarantine_version_id`, `quarantine_etag`, 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 | Доводит `202 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-спеках.
Для Safety: Product Owner принимает generic UX; Safety Service Owner — v2 contract/cutover; Rule Pack Owner — rules/corpus; Security Owner — monitor→deny и risks; Operations Owner — VM2/incident/restore. Назначения ролей фиксируются в release checklist.
---
## 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`.
16. Любой Safety deny создаёт ровно одну company-реплику с mnemonic `safety.chat.blocked`; исходный blocked text редактируется, internal `rule_id` не виден клиенту.
17. В emergency MOCK normal checks не выполняются: отдельные fixed policy для text/file дают только sync allow/deny. Mode не виден клиенту; mock file allow хранится как `scan_status=bypassed`, а включение доступно `deploy` только через root-owned helper/restart и не имеет auto-expiry.
17. Semantic prompt-injection RU/EN возвращает allow + monitor audit; active content, URL policy и malware остаются hard deny.
18. `files=unavailable` не ломает text-only чат; `links=unavailable` блокирует только text с URL; Redis Safety outage не выключает core.
19. File async проходит `202` внутри api-backend до sticky final; public pending клиенту не возвращается.
20. EICAR, malformed/polyglot/encrypted/active PDF дают deny; dependency timeout даёт `503`, а не blocked.
21. Link pipeline не выполняет HTTP fetch; NXDOMAIN разрешяется с monitor, private/metadata IP блокируется.
22. S3 overwrite получает `412`; wrong version/ETag и conditional promote mismatch запрещают delivery.
23. Load acceptance module-05 §15.4 проходит: 10 text/s, 2 file/s, 5 slots, ≤100 pending; availability SLO в MVP не задаётся.
24. VM2 cutover/rollback gates module-10 пройдены; v1 stub не считается production control.
---
## 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 |
| D13 | Generic Safety deny: company-реплика `safety.chat.blocked`, blocked text redacted, rule не раскрывается | §6.2, module-01 M8 |
| D14 | Emergency MOCK: независимые forced text/file allow/deny, root-owned helper для `deploy`, без auto-expiry | module-05 §2.3, module-10 |
| D14 | Semantic rules monitor-only; NXDOMAIN monitor allow | module-05 |
| D15 | Performance acceptance без availability SLO: 10 text/s, 2 file/s, 5 slots, 100 pending | module-05 §15.4 |
### 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 |
| Q4 | Создание нового dialog сразу после `closed` — всегда разрешено или по бизнес-правилу «сессия поддержки» | MVP: разрешить, пока соблюдён unique active | Продукт / поддержка |
| Q5 | Stub v1 расходится с target v2 | Для production — только `200/202/403` и terminal failed `503`; stub изолирован adapter-ом до cutover | 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`.