Добавлен OTLP-провайдер, реализовано отбрасывание метрик и трейсов в observability + добавлен перезапуск nginx при пересборке контейнеров (ошибка, когда докер меняет адреса сервисов)

This commit is contained in:
mi
2026-07-29 15:15:40 +03:00
parent 3ed7239efa
commit 41e19005fb
43 changed files with 3016 additions and 83 deletions
@@ -0,0 +1,617 @@
# Бизнес-постановка: Обмен сообщениями (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 → отложенная отправка |
| Главная: популярные вопросы | Тап = автоотправка текста вопроса (тот же поток, что ручной ввод) |
| Кнопка «Чат» | Открывает текущий активный диалог или создаёт его при отсутствии |
| Кнопка «Оператор» | `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.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 **не** уходит.
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`.