Проект разделен на два репозитория
This commit is contained in:
@@ -0,0 +1,638 @@
|
||||
# Бизнес-постановка: Обмен сообщениями (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 п.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).
|
||||
- До 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`.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,555 @@
|
||||
# Бизнес-постановка: Пользователь (User)
|
||||
|
||||
**Статус:** v1 — консолидация принятых решений из `HAN_chat_specification` (`arch-00`…`arch-05`, `module-01`, `module-08`); открытые вопросы зафиксированы в §17
|
||||
**Продукт:** HAN Chat (клиентское приложение + `api-backend` + Keycloak)
|
||||
**Источники:** архитектура `HAN_chat_specification`; макет Figma (**не канон** — только визуализация; при расхождении приоритет у этого ТЗ и arch-документов)
|
||||
**Связанный backlog:** кнопка «Войти»; история устройств входа; дифференцированные ошибки OTP; debounce SMS; тестовый пользователь с фиксированным SMS-входом
|
||||
**Смежно:** уведомления (контуры G/P), чат, согласия, UX-сессия, CRM Contact через `bitrix-sync`
|
||||
|
||||
**Нормативная часть — §1–§16.** §17 — ненормативный журнал решений и открытых вопросов; при расхождении с §1–§16 приоритет у §1–§16. При расхождении этого документа с arch/module после их обновления — приоритет у arch/module до синхронизации.
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Дать клиенту устойчивую идентичность в HAN Chat: вход по подтверждённому телефону, локальную карточку пользователя в App DB, согласия, readonly-профиль в ЛК и связь с Contact в Bitrix24 — без смешения гостевого просмотра и персонального кабинета.
|
||||
|
||||
Гость изучает сервис без записи в App DB. Авторизованный клиент получает персональные данные, чат, персональные уведомления и профиль. Auth-идентичность принадлежит Keycloak; приложение владеет бизнес-карточкой пользователя, согласиями и кэшем профиля для UI.
|
||||
|
||||
---
|
||||
|
||||
## 2. Два режима клиента
|
||||
|
||||
| Режим | Кто это | Идентичность на бэкенде | Что доступно |
|
||||
|---|---|---|---|
|
||||
| **G. Гость** | Клиент без действующего JWT | Нет `UserIdentity`; опциональный локальный `guest_session_id` только на устройстве | UI + `GET /api/v1/public/*`; гостевые уведомления (контур G) |
|
||||
| **A. Авторизованный** | Клиент с валидным access token и выполненным `bootstrap` | `user_identities` (`keycloak_sub` ↔ JWT `sub`) | JWT API: чат, профиль, согласия, персональные уведомления, UX-сессия |
|
||||
|
||||
Правила:
|
||||
|
||||
1. До успешного OTP + `bootstrap` клиент — только гость. Write-endpoint (`consents`, `session-start`, чат, профиль и т.д.) **требуют JWT**.
|
||||
2. После авторизации гостевой контент **не** переносится в персональный (как в уведомлениях: G не мигрирует в P).
|
||||
3. `guest_session_id` **не** является auth и **не** открывает write API.
|
||||
4. Наличие refresh token в secure storage позволяет вернуться в режим **A** без OTP (§6.3); отсутствие/истечение refresh → снова гость до следующего защищённого действия.
|
||||
5. Access token проверяет `api-backend`; refresh выполняет **только frontend** через Keycloak. Backend refresh **не** делает.
|
||||
|
||||
---
|
||||
|
||||
## 3. Границы релиза
|
||||
|
||||
### 3.1. В scope
|
||||
|
||||
- OTP-only вход по номеру телефона (Keycloak; mock до controlled SMS cutover).
|
||||
- OIDC Authorization Code + PKCE; refresh / logout; silent return без OTP при валидном refresh.
|
||||
- Локальный `find-or-create` `UserIdentity` + минимальный `ClientProfile` через `POST /api/v1/auth/bootstrap`.
|
||||
- Согласия: обязательные `personal_data` + `user_agreement`, опциональный `marketing`; фиксация версий в `user_consents`.
|
||||
- Readonly блочный профиль `GET /api/v1/me`; зарезервированный `GET /api/v1/me/documents` (MVP может быть пустым).
|
||||
- Аналитическая `UxSession` для авторизованного клиента (`session-start`, заголовок `X-Ux-Session-Id`).
|
||||
- Асинхронный map/create Contact в Bitrix24 по телефону через `sync_queue` (не блокирует вход).
|
||||
- Нормализация телефона в E.164; phone claim только из JWT, не из body клиента.
|
||||
- Ownership: все пользовательские ресурсы адресуются через `user_id` из JWT.
|
||||
|
||||
### 3.2. Вне scope
|
||||
|
||||
- Пароль, email-OTP, social login, magic link.
|
||||
- Редактирование профиля клиентом (PATCH/PUT отсутствуют).
|
||||
- Доставка документов компании в блок «Документы» (post-MVP; API зарезервирован).
|
||||
- История устройств входа (backlog п.19) — модель и UI позже.
|
||||
- Смена телефона как продуктовый self-service flow (policy Keycloak есть; продуктовый UX — отдельно).
|
||||
- Merge/reassignment двух `sub` на один телефон — только administrative policy, не side effect login.
|
||||
- Гостевая запись согласий и UX-сессии в App DB.
|
||||
- Создание `UserIdentity` / `ClientProfile` сервисом `bitrix-sync` (sync не участвует в OTP-flow).
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменения UI (относительно гостевого экрана)
|
||||
|
||||
| Место | Гость | Авторизованный |
|
||||
|---|---|---|
|
||||
| Главная | Публичный контент, гостевые уведомления | Персональные уведомления, чат доступен |
|
||||
| Отправка сообщения / популярный вопрос | Сначала согласия → OTP → bootstrap → отправка отложенного текста | Штатная отправка |
|
||||
| Центр уведомлений | Auth-gate «Авторизоваться» | Список персональных |
|
||||
| Профиль | Недоступен / ведёт на вход | Readonly блоки «Личные данные», «Документы» |
|
||||
| Кнопка «Войти» | Запускает поток авторизации | Скрыта / заменена профилем (по макету) |
|
||||
| Выход | — | Очистка tokens → гостевой UI |
|
||||
|
||||
Визуал — по Figma. Figma не канон поведения и состава полей профиля: канон — §5.5 и arch-01.
|
||||
|
||||
---
|
||||
|
||||
## 5. Бизнес-модель
|
||||
|
||||
### 5.1. Слои идентичности (не смешивать)
|
||||
|
||||
| Слой | Где живёт | Что хранит | Master |
|
||||
|---|---|---|---|
|
||||
| **IdP user** | Keycloak (`keycloak` schema) | Realm user, phone verified, sessions, tokens | Keycloak |
|
||||
| **UserIdentity** | App DB `user_identities` | Локальный `user_id`, связь `keycloak_sub`, кэш auth-телефона, `last_login_at` | Keycloak для телефона/`sub`; App DB для бизнес-FK |
|
||||
| **ClientProfile** | App DB `client_profiles` | Кэш полей UI без CRM-идентификаторов | UI-поля — последнее успешно синхронизированное значение (входящий поток MVP — Bitrix24); auth-телефон инициирует sync, но master телефона — Keycloak |
|
||||
| **Bitrix Contact** | CRM Bitrix24 | Карточка клиента в CRM | Bitrix24 для CRM-полей; связь `user_id ↔ b24_id` хранится только в `bitrix_sync.entity_external_mapping` |
|
||||
| **Гость** | Только устройство | UI-state, локальные согласия до OTP, опционально `guest_session_id` | Нет серверной записи |
|
||||
|
||||
**Инвариант:** один verified phone ↔ один active Keycloak `sub`. Один `sub` ↔ одна active `UserIdentity`. Один `user_id` ↔ один `ClientProfile`.
|
||||
|
||||
### 5.2. Связанные сущности (часть домена «пользователь», но не сам User)
|
||||
|
||||
| Сущность | Роль | Когда появляется |
|
||||
|---|---|---|
|
||||
| `UserConsent` | Факт принятия документа конкретной версии | `bootstrap` или `POST /consents` |
|
||||
| `UxSession` | Аналитический период активности | `session-start` **только** у авторизованного |
|
||||
| `Dialog` / `Message` | Чат с оператором | После auth, лениво при первом сообщении |
|
||||
| Notification (P) | Персональные уведомления | `user_id` NOT NULL |
|
||||
|
||||
`UxSession` **не** является механизмом авторизации и **не** заменяет JWT.
|
||||
|
||||
### 5.3. Согласия
|
||||
|
||||
| Тип | Обязательность (seed) | Документ / URL | Версия |
|
||||
|---|---|---|---|
|
||||
| `personal_data` | да (`consent.personal_data.required=true`) | `consent.personal_data.document_url` (+ политика `consent.privacy_policy.document_url`) | `consent.personal_data.version` |
|
||||
| `user_agreement` | да | `consent.user_agreement.document_url` | `consent.user_agreement.version` |
|
||||
| `marketing` | нет | `consent.marketing.document_url` | `consent.marketing.version` |
|
||||
|
||||
Правила:
|
||||
|
||||
1. Pop-up согласий показывается **до** OTP; до получения JWT факт принятия хранится **только на клиенте**.
|
||||
2. Серверная фиксация — в `bootstrap` (атомарно с созданием пользователя) или позже через `POST /api/v1/consents` при смене версий документов.
|
||||
3. Запись `UserConsent` **immutable**: unique `(user_id, consent_type, document_version)`; исправление — новая версия документа или administrative action с audit.
|
||||
4. Обязательные согласия без `accepted: true` → `403 consents_required`; вход в ЛК / write API с непринятыми актуальными обязательными версиями блокируется.
|
||||
5. Keycloak consent screen **не** заменяет продуктовые согласия API.
|
||||
|
||||
### 5.4. Auth-телефон
|
||||
|
||||
- Единственный канал MVP: номер телефона + OTP.
|
||||
- Нормализация: libphonenumber → canonical E.164.
|
||||
- В App DB телефон пишется **только** из JWT claims при `bootstrap` / обновлении identity, **никогда** из body клиента.
|
||||
- Порядок claim: `phone_number`, иначе `preferred_username` только если значение валидно как E.164.
|
||||
- Отсутствие/невалидность при bootstrap → `400 phone_claim_missing`.
|
||||
- Утечка существования номера запрещена на стороне Keycloak (одинаковый внешний ответ для нового/существующего).
|
||||
- В `ClientProfile` при создании копируется в `russian_phone` (минимальный профиль); дальнейшее обогащение — из CRM sync.
|
||||
|
||||
### 5.5. Профиль (UI)
|
||||
|
||||
Блочная модель. Редактирование клиентом **недоступно**.
|
||||
|
||||
**Блок «Личные данные»:**
|
||||
|
||||
| Поле | Источник отображения | Примечание |
|
||||
|---|---|---|
|
||||
| ФИО (`full_name`) | `client_profiles` | Может быть `null` до sync из Bitrix24 |
|
||||
| Гражданство (`citizenship`) | `client_profiles` | `null` до заполнения |
|
||||
| Телефон РФ (`russian_phone`) | `client_profiles` | При bootstrap = auth-телефон |
|
||||
| Зарубежный телефон (`foreign_phone`) | `client_profiles` | Опционально |
|
||||
| Email (`email`) | `client_profiles` | Опционально |
|
||||
|
||||
**Блок «Документы»:**
|
||||
|
||||
- перечень документов компании, дата, наименование, скачивание;
|
||||
- в MVP список может быть пустым; доставка из Bitrix24 — post-MVP;
|
||||
- API: `GET /api/v1/me/documents`, `GET /api/v1/documents/{id}`, `.../download-url` с audit.
|
||||
|
||||
Макет Figma может показывать дополнительные секции (патент, РВП и т.п.) — это **не** канон MVP-модели данных; расширение блоков — отдельное решение.
|
||||
|
||||
### 5.6. Жизненный цикл пользователя
|
||||
|
||||
| Состояние | Условие | Что видит клиент |
|
||||
|---|---|---|
|
||||
| Гость | Нет валидного JWT | Публичный UI |
|
||||
| OTP in progress | Идёт challenge в Keycloak | Экраны телефона / кода |
|
||||
| Authenticated, bootstrap pending | Есть JWT, нет local `UserIdentity` | Frontend обязан вызвать `bootstrap`; прочие protected → `409` «bootstrap required» |
|
||||
| Authenticated, ready | Есть `UserIdentity` + актуальные обязательные согласия | Полный ЛК |
|
||||
| Soft-deleted | `record_status='D'` на identity (админ) | Доступ запрещён; детали — operational policy |
|
||||
|
||||
Бизнес-«удаление аккаунта» клиентом в MVP **не** моделируется. Soft-delete — административный контур (arch-05).
|
||||
|
||||
### 5.7. Константы (`app_settings`)
|
||||
|
||||
| Ключ | Смысл | Default / seed |
|
||||
|---|---|---|
|
||||
| `auth.phone.enabled` | Вход по телефону | `true` |
|
||||
| `auth.password.enabled` | Пароль | `false` |
|
||||
| `otp.phone.max_send_attempts_per_24h` | Лимит отправок OTP | `3` |
|
||||
| `otp.phone.min_seconds_between_attempts` | Минимальный интервал между отправками | `30` |
|
||||
| `otp.phone.max_verify_attempts` | Лимит проверок кода | `5` |
|
||||
| `otp.phone.code_length` | Длина кода | `6` |
|
||||
| `otp.phone.ttl_seconds` | TTL кода | `60` |
|
||||
| `otp.phone.sms_order_timeout_ms` | Таймаут заказа SMS | `3000` |
|
||||
| `consent.*` | URL/версии/required флагов согласий | см. arch-04 |
|
||||
| `ux.session.idle_timeout_minutes` | Idle → новая UX-сессия | `30` |
|
||||
|
||||
Счётчики OTP ведёт **Keycloak/SPI**, не `api-backend`. Продуктовые `otp.phone.*` Keycloak читает через settings bridge `GET /internal/settings/v1/otp`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Поведение UI и сценарии
|
||||
|
||||
### 6.1. Гостевой режим
|
||||
|
||||
1. Клиент открывает приложение → гостевой UI.
|
||||
2. Доступен только `GET /api/v1/public/*` (+ статика).
|
||||
3. Согласия и `session-start` в App DB **не** пишутся.
|
||||
4. Попытка защищённого действия (сообщение, Центр уведомлений, профиль) → поток авторизации.
|
||||
|
||||
### 6.2. Поток первой авторизации (OTP)
|
||||
|
||||
1. Триггер: отправка сообщения / популярный вопрос / «Войти» / иное действие, требующее auth.
|
||||
2. Pop-up согласий; обязательные должны быть приняты локально.
|
||||
3. Форма телефона → Keycloak OTP-flow (mock или real SMS через `sms-service`).
|
||||
4. Успешная проверка OTP → tokens (Authorization Code + PKCE).
|
||||
5. `POST /api/v1/auth/bootstrap` с локальными согласиями и `device` metadata.
|
||||
6. `POST /api/v1/analytics/session-start` при необходимости новой UX-сессии.
|
||||
7. Триггер БД ставит `contact.map_or_create` в `sync_queue` (асинхронно; ошибка CRM **не** откатывает вход).
|
||||
8. Frontend продолжает исходное действие (в т.ч. отложенное сообщение / популярный вопрос).
|
||||
|
||||
**UX-инвариант (backlog п.21):** ошибка отправки отложенного сообщения после успешного bootstrap **не** должна выглядеть как «не удалось завершить вход». Вход завершён на шаге 5–6; ошибка Bitrix/чата показывается в контексте чата.
|
||||
|
||||
### 6.3. Возврат без OTP
|
||||
|
||||
1. Есть валидный refresh token → Refresh Token Grant → access token.
|
||||
2. При необходимости — `session-start`.
|
||||
3. OTP не показывается.
|
||||
4. Нет/истёк refresh → гость до следующего защищённого действия.
|
||||
|
||||
### 6.4. Поддержание сессии (tokens)
|
||||
|
||||
- Frontend проактивно обновляет access token (~60 с до `exp`), single-flight.
|
||||
- Успешный refresh **не** создаёт новую UX-сессию.
|
||||
- `401` от API → один refresh + retry исходного запроса; провал refresh → очистка tokens → гость.
|
||||
- То же для WebSocket `/api/v1/realtime`.
|
||||
|
||||
### 6.5. UX-сессия
|
||||
|
||||
Новая `UxSession` только при:
|
||||
|
||||
| `start_reason` | Когда |
|
||||
|---|---|
|
||||
| `first_launch` | В памяти нет `ux_session_id` |
|
||||
| `cold_start` | Kill app / закрытие вкладки |
|
||||
| `idle_timeout` | Простой > `ux.session.idle_timeout_minutes` |
|
||||
|
||||
`ux_session_id` хранится **только в памяти** (не в localStorage). Передаётся как `X-Ux-Session-Id`. Отсутствие заголовка API не блокирует (кроме endpoint, где id обязателен).
|
||||
|
||||
### 6.6. Профиль
|
||||
|
||||
- Открывается только авторизованным.
|
||||
- Данные — `GET /api/v1/me`; поля могут быть частично пустыми до CRM sync.
|
||||
- Редактирование недоступно; изменение ФИО/email и т.п. — через процессы компании (Bitrix24 → sync).
|
||||
- Документы — отдельный блок; скачивание с audit.
|
||||
|
||||
### 6.7. Выход
|
||||
|
||||
1. Frontend инициирует logout у Keycloak (revocation по policy модуля).
|
||||
2. Очищает access/refresh tokens и in-memory UX-сессию.
|
||||
3. UI переходит в гостевой режим.
|
||||
4. Локальный `UserIdentity` в App DB **не** удаляется.
|
||||
|
||||
---
|
||||
|
||||
## 7. Матрицы поведения
|
||||
|
||||
### 7.1. Что требует auth
|
||||
|
||||
| Действие | Гость | Авторизованный |
|
||||
|---|---|---|
|
||||
| `GET /api/v1/public/*` | да | да |
|
||||
| Просмотр главной / гостевых уведомлений | да | нет (после входа — только P) |
|
||||
| Отправка сообщения / вложение | нет → OTP | да |
|
||||
| `POST /auth/bootstrap` | нет (нужен JWT после OTP) | да (идемпотентно) |
|
||||
| `POST /consents`, `session-start` | нет | да |
|
||||
| `GET /me`, чат, персональные уведомления | нет | да |
|
||||
| `WS /api/v1/realtime` | нет | да |
|
||||
|
||||
### 7.2. Источник истины полей
|
||||
|
||||
| Поле / факт | Master | Куда кэшируется |
|
||||
|---|---|---|
|
||||
| `sub` / существование IdP user | Keycloak | `user_identities.keycloak_sub` |
|
||||
| Auth-телефон | Keycloak | `user_identities.phone_number`, seed `client_profiles.russian_phone` |
|
||||
| Согласия (версия + accepted) | App DB `user_consents` | — |
|
||||
| ФИО, гражданство, email | Последний успешный sync (MVP: Bitrix → App) | `client_profiles` |
|
||||
| `foreign_phone` | Не синхронизируется в первом релизе | `client_profiles` |
|
||||
| Связь с Contact | `bitrix-sync` | Только `bitrix_sync.entity_external_mapping`; в App DB не кэшируется |
|
||||
| Tokens / auth session | Keycloak | secure storage на клиенте |
|
||||
| `ux_session_id` | App DB + память клиента | заголовок запросов |
|
||||
|
||||
### 7.3. Bootstrap — идемпотентность
|
||||
|
||||
| Повторный вызов | Результат |
|
||||
|---|---|
|
||||
| Тот же `sub`, те же версии согласий | `200`, тот же `user_id`; `last_login_at` обновляется; дублей consent нет |
|
||||
| Тот же `sub`, новые версии согласий | Новые immutable строки consent + update identity |
|
||||
| JWT без phone claim | `400 phone_claim_missing` |
|
||||
| Обязательные consents не accepted | `403 consents_required` |
|
||||
|
||||
Application-код **не** пишет в `sync_queue`: задачи создают триггеры на insert/update `UserIdentity` / `ClientProfile`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Идентификация
|
||||
|
||||
| Идентификатор | Назначение |
|
||||
|---|---|
|
||||
| `keycloak_sub` | Subject JWT; ключ find-or-create |
|
||||
| `user_id` | PK `user_identities`; FK всех персональных сущностей App DB |
|
||||
| `phone_number` | Auth-телефон E.164 |
|
||||
| `guest_session_id` | Локальный UUID устройства; не auth |
|
||||
| `ux_session_id` | Аналитическая сессия |
|
||||
| `b24_id` | Внутренний идентификатор Contact; используется только `bitrix-sync` |
|
||||
| `device_id` | Opaque id устройства в bootstrap / session-start; в audit/log не копируется как PII |
|
||||
|
||||
Публичные id — **UUID** (в App DB предпочтительно UUID v7, как в остальных доменах).
|
||||
|
||||
---
|
||||
|
||||
## 9. API
|
||||
|
||||
Общие конвенции — arch-02.
|
||||
|
||||
### 9.1. Клиентские (JWT)
|
||||
|
||||
| Метод и путь | Назначение |
|
||||
|---|---|
|
||||
| `POST /api/v1/auth/bootstrap` | Find-or-create пользователя + согласия |
|
||||
| `POST /api/v1/consents` | Повторная фиксация версий согласий |
|
||||
| `POST /api/v1/analytics/session-start` | Новая `UxSession` |
|
||||
| `GET /api/v1/me` | Readonly блочный профиль |
|
||||
| `GET /api/v1/me/documents` | Список документов (MVP может быть пустым) |
|
||||
| `GET /api/v1/documents/{id}` | Metadata документа (owner only) |
|
||||
| `GET /api/v1/documents/{id}/download-url` | Presigned GET + audit |
|
||||
|
||||
Auth у Keycloak: публичные OIDC endpoints через `/auth/*` (не часть `api-backend`).
|
||||
|
||||
### 9.2. Public (без JWT)
|
||||
|
||||
| Метод и путь | Назначение |
|
||||
|---|---|
|
||||
| `GET /api/v1/public/settings` (и связанные public) | Флаги auth, URL/версии согласий, OTP UI-параметры по `is_public` |
|
||||
|
||||
### 9.3. Internal (смежные)
|
||||
|
||||
| Метод и путь | Кто → кто | Назначение |
|
||||
|---|---|---|
|
||||
| `GET /internal/settings/v1/otp` | Keycloak SPI → api-backend | Продуктовые OTP limits |
|
||||
| `POST /internal/sms/v1/send` | Keycloak → sms-service | Заказ SMS OTP (real mode) |
|
||||
|
||||
### 9.4. Ошибки (домен пользователя)
|
||||
|
||||
| Код | HTTP | Когда |
|
||||
|---|---|---|
|
||||
| `phone_claim_missing` | 400 | Нет канонического телефона в JWT при bootstrap |
|
||||
| `validation_error` | 400 | Невалидные версии/тело согласий или device |
|
||||
| `unauthorized` | 401 | Нет/невалиден JWT |
|
||||
| `consents_required` | 403 | Обязательные согласия не приняты |
|
||||
| `resource_state_conflict` | 409 | Protected endpoint до bootstrap («bootstrap required»); **открытый вопрос TBD-1** по унификации с `404` для consents |
|
||||
| `profile_not_found` | 404 | Профиль не найден (по контракту envelope) |
|
||||
| `rate_limit_exceeded` | 429 | Превышен лимит |
|
||||
|
||||
Дифференцированные тексты ошибок OTP на UI (неверный код / истёк / лимит send / лимит verify) — backlog п.10; контракт Keycloak/frontend уточняется отдельно, в этом ТЗ фиксируется требование продукта.
|
||||
|
||||
### 9.5. Rate limiting
|
||||
|
||||
| Зона | Identity |
|
||||
|---|---|
|
||||
| Auth edge (`nginx`) | IP |
|
||||
| bootstrap / consents / session-start | user + IP |
|
||||
| OTP product limits | phone (Keycloak counters) |
|
||||
|
||||
---
|
||||
|
||||
## 10. Модель данных (схема `han_app`)
|
||||
|
||||
Общие правила — module-01 §9.1 / arch-05: UUID PK, `timestamptz` UTC, common fields, soft-delete `A`/`D`, FK `ON DELETE RESTRICT`.
|
||||
|
||||
### 10.1. `user_identities`
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | uuid PK | `user_id` |
|
||||
| `keycloak_sub` | varchar(255) NOT NULL UNIQUE | JWT `sub` |
|
||||
| `phone_number` | varchar(32) NOT NULL | E.164 из JWT |
|
||||
| `last_login_at` | timestamptz NOT NULL | Обновляется на bootstrap |
|
||||
| common fields | обязательны | |
|
||||
|
||||
Индексы: unique `keycloak_sub`; index на `phone_number` для CRM map. **Телефон в App DB не unique:** identity master — Keycloak; временный конфликт при merge/миграции допустим на уровне данных, но продуктово один phone = один active `sub`.
|
||||
|
||||
### 10.2. `user_consents`
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | uuid PK | |
|
||||
| `user_id` | uuid FK | |
|
||||
| `ux_session_id` | uuid NULL | Если сессия уже есть |
|
||||
| `consent_type` | varchar | `personal_data` \| `user_agreement` \| `marketing` |
|
||||
| `document_version` | varchar | Версия из `app_settings` |
|
||||
| `accepted` | boolean | |
|
||||
| `accepted_at` | timestamptz | |
|
||||
| `client_ip` | inet | |
|
||||
| `user_agent_hash` | varchar | |
|
||||
| device snapshot | jsonb / поля | По module-01 (`device_json` и т.п.) |
|
||||
| common fields | обязательны | |
|
||||
|
||||
Unique `(user_id, consent_type, document_version)`. Записи immutable.
|
||||
|
||||
### 10.3. `client_profiles`
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | uuid PK | |
|
||||
| `user_id` | uuid UNIQUE FK | 1:1 с identity |
|
||||
| `full_name` | varchar NULL | |
|
||||
| `citizenship` | varchar NULL | |
|
||||
| `russian_phone` | varchar NULL | Seed из auth-телефона |
|
||||
| `foreign_phone` | varchar NULL | |
|
||||
| `email` | varchar NULL | |
|
||||
| `source_updated_at` | timestamptz NULL | Метка источника sync |
|
||||
| common fields | обязательны | |
|
||||
|
||||
CRM Contact ID и mapping в `client_profiles` отсутствуют. PII не попадает в логи и generic audit payload.
|
||||
|
||||
### 10.4. `ux_sessions`
|
||||
|
||||
`id` = `ux_session_id`; `user_id`; `start_reason`; `platform`; `app_version`; `device_id`; `started_at`; common fields.
|
||||
|
||||
### 10.5. Триггеры sync
|
||||
|
||||
| Событие | `task_type` |
|
||||
|---|---|
|
||||
| Insert active `UserIdentity` / `ClientProfile` без mapping | `contact.map_or_create` |
|
||||
| Изменение tracked profile / auth-phone полей | `contact.update` |
|
||||
|
||||
Подавление эха: GUC `han.sync_suppress` при записи из `bitrix-sync`. Ошибка CRM не откатывает bootstrap и чат.
|
||||
|
||||
---
|
||||
|
||||
## 11. Фоновые и смежные процессы
|
||||
|
||||
| Процесс | Владелец | Связь с пользователем |
|
||||
|---|---|---|
|
||||
| OTP challenge / counters / expiry | Keycloak SPI | До появления App user |
|
||||
| SMS order / delivery journal | `sms-service` | Только доставка кода |
|
||||
| `contact.map_or_create` / `contact.update` / `contact.deactivate` | `bitrix-sync` | После bootstrap / изменения телефона / деактивации |
|
||||
| `contact.rebind` | `bitrix-sync` | Audited административное исправление ошибочного mapping |
|
||||
| Token refresh / logout | Frontend + Keycloak | Не трогает App DB identity |
|
||||
| Retention UX-сессий (если введён) | ops / module | Не удаляет `UserIdentity` |
|
||||
|
||||
---
|
||||
|
||||
## 12. Audit и observability
|
||||
|
||||
| `event_type` | Actor | Когда |
|
||||
|---|---|---|
|
||||
| `auth.bootstrap` | user | Успешный bootstrap |
|
||||
| `consent.recorded` | user | Запись согласий (bootstrap или `/consents`) |
|
||||
| `session_start` | user | Новая UX-сессия |
|
||||
| `document.download_url_issued` | user | Скачивание документа профиля |
|
||||
| OTP security events | Keycloak | Send/verify attempts (schema `keycloak`, phone HMAC/masked) |
|
||||
|
||||
В audit **нет:** полного phone/email/name в свободном тексте логов общего контура, OTP raw code, tokens, presigned URL.
|
||||
|
||||
Метрики (минимум): число bootstrap/сутки, доля `consents_required`, доля `phone_claim_missing`. Latency map Contact и доля пользователей без active mapping спустя N минут считаются `bitrix-sync` по собственной схеме.
|
||||
|
||||
---
|
||||
|
||||
## 13. Хранение данных и PII
|
||||
|
||||
- `UserIdentity`, `ClientProfile`, `UserConsent` — прикладные строки, soft-delete, физическое удаление запрещено (arch-05).
|
||||
- Auth-мастер PII телефона — Keycloak; App DB держит кэш для FK/CRM/UI.
|
||||
- Согласия хранятся бессрочно как юридически значимый журнал (immutable rows).
|
||||
- Гостевые локальные согласия на устройстве до OTP **не** являются серверным журналом и при сбое до bootstrap могут быть потеряны — клиент проходит согласия снова.
|
||||
- Right-to-erasure / удаление аккаунта клиентом — вне scope MVP; потребует отдельной политики по IdP + App DB + CRM.
|
||||
|
||||
---
|
||||
|
||||
## 14. Смежные сервисы
|
||||
|
||||
| Компонент | Ответственность в домене User |
|
||||
|---|---|
|
||||
| **Frontend** | Гость/ЛК, согласия UI, OTP UX, tokens, refresh, bootstrap/session-start, профиль readonly, отложенное сообщение после входа |
|
||||
| **Keycloak** | IdP, OTP, phone uniqueness, tokens, sessions |
|
||||
| **api-backend** | Bootstrap, consents, me/profile, JWT validation, ownership, settings bridge OTP |
|
||||
| **sms-service** | Durable order SMS (real mode) |
|
||||
| **bitrix-sync** | Map/update Contact; **не** создаёт UserIdentity |
|
||||
| **nginx** | `/auth/*`, edge rate limit auth |
|
||||
| **App DB** | Таблицы §10, триггеры sync |
|
||||
|
||||
Порядок работ (если дорабатывать домен): (1) Keycloak OTP + phone claims → (2) bootstrap + consents + identity/profile → (3) session-start → (4) me/profile UI → (5) CRM map → (6) documents post-MVP.
|
||||
|
||||
---
|
||||
|
||||
## 15. Влияние на arch-документы
|
||||
|
||||
| Документ | Статус относительно этой постановки |
|
||||
|---|---|
|
||||
| `arch-00-glossary.md` | Термины `UserIdentity`, `ClientProfile`, `UserConsent`, `UxSession`, `guest_session_id`, `keycloak_sub` уже заданы |
|
||||
| `arch-01-system-architecture.md` | Потоки гостя, OTP, возврата, профиля — канон сценариев |
|
||||
| `arch-02-api-contracts.md` | Контракты bootstrap / consents / session-start / me |
|
||||
| `arch-04-settings-and-content.md` | `auth.*`, `otp.phone.*`, `consent.*`, `ux.session.*` |
|
||||
| `module-01-api-backend.md` | Таблицы и алгоритмы bootstrap |
|
||||
| `module-08-keycloak.md` | OTP-only phone flow |
|
||||
| `module-07-bitrix-sync.md` | Обработка `contact.*` задач |
|
||||
|
||||
Этот документ **не заменяет** module-спеки; он собирает бизнес-смысл сущности «Пользователь» для аналитики и смежных фич (уведомления, чат, документы).
|
||||
|
||||
---
|
||||
|
||||
## 16. Критерии приёмки
|
||||
|
||||
1. Гость видит только public API; write без JWT недоступен; `guest_session_id` не открывает API.
|
||||
2. Вход только по телефону + OTP; пароль/email/social отсутствуют.
|
||||
3. После OTP `bootstrap` создаёт/находит `UserIdentity`, минимальный `ClientProfile`, пишет согласия; телефон берётся из JWT, не из body.
|
||||
4. Повторный bootstrap идемпотентен; `last_login_at` обновляется.
|
||||
5. Без обязательных согласий — `403 consents_required`; без phone claim — `400 phone_claim_missing`.
|
||||
6. Protected endpoint до bootstrap — безопасный отказ (`409` до закрытия TBD-1).
|
||||
7. Возврат с валидным refresh — без OTP; провал refresh — гостевой UI.
|
||||
8. `session-start` только с JWT; в гостевом режиме не вызывается; idle/cold/first_launch создают новую UX-сессию.
|
||||
9. `GET /me` отдаёт блочный readonly профиль; PATCH/PUT нет.
|
||||
10. Ошибка CRM sync не ломает вход; Contact мапится асинхронно.
|
||||
11. Один active phone ↔ один active `sub` на стороне Keycloak; merge не происходит молча при login.
|
||||
12. Отложенное сообщение / популярный вопрос после auth уходит штатно; ошибка доставки не маскируется под ошибку входа.
|
||||
13. Выход очищает tokens и возвращает в гостевой UI, не удаляя `UserIdentity`.
|
||||
14. PII не светится в обычных логах/audit payload; OTP code не логируется.
|
||||
|
||||
---
|
||||
|
||||
## 17. Журнал решений и открытых вопросов (ненормативно)
|
||||
|
||||
### 17.1. Принятые решения
|
||||
|
||||
| # | Решение | Раздел / источник |
|
||||
|---|---|---|
|
||||
| D1 | Два режима: гость и авторизованный; гостевые данные в App DB не пишутся | §2, arch-01 |
|
||||
| D2 | Слои IdP / UserIdentity / ClientProfile / Bitrix Contact разделены; master auth — Keycloak | §5.1 |
|
||||
| D3 | OTP-only phone; password disabled | §3, module-08 |
|
||||
| D4 | Согласия продуктовые в API; Keycloak их не заменяет; серверная запись только после JWT | §5.3 |
|
||||
| D5 | Телефон только из JWT claims | §5.4, arch-02 |
|
||||
| D6 | Профиль readonly и блочный; документы — отдельный блок, доставка post-MVP | §5.5 |
|
||||
| D7 | Bootstrap атомарный + идемпотентный; sync через триггеры БД | §7.3 |
|
||||
| D8 | UX-сессия ≠ auth; только для авторизованных; хранение id в памяти | §6.5, arch-00 |
|
||||
| D9 | CRM не блокирует авторизацию | §6.2 |
|
||||
| D10 | Один verified phone = один active `sub` | module-08 |
|
||||
|
||||
### 17.2. Открытые вопросы
|
||||
|
||||
| # | Вопрос | Предложение | Влияние |
|
||||
|---|---|---|---|
|
||||
| Q1 | Единый код для protected endpoint до bootstrap: `409` vs `404` (TBD-1 module-01) | Оставить `409 resource_state_conflict` | OpenAPI, клиентский UX |
|
||||
| Q2 | Дифференцированные ошибки OTP на UI (backlog п.10) | Зафиксировать словарь кодов Keycloak → frontend texts | module-08 + frontend |
|
||||
| Q3 | История устройств входа (backlog п.19) | Отдельная сущность/таблица, не смешивать с `UxSession` | Новая постановка |
|
||||
| Q4 | Debounce/backoff SMS после интеграции провайдера (backlog п.11) | Надстройка над `otp.phone.min_seconds_between_attempts` | Keycloak SPI |
|
||||
| Q5 | Тестовый пользователь с фиксированным SMS (backlog п.16) | Операционный allow-list / mock per-phone, не дырка в prod limits | ops + module-08 |
|
||||
| Q6 | Продуктовый self-service смены телефона | Позже: re-auth + OTP нового номера + invalidate sessions | Keycloak + bootstrap |
|
||||
| Q7 | Клиентское удаление аккаунта / right-to-erasure | Вне MVP; отдельная юридическая и техническая постановка | IdP + App + CRM |
|
||||
| Q8 | Расширение блоков профиля сверх «Личные данные» / «Документы» (как в Figma-моках) | Только после продуктового решения; Figma не канон | UI + `client_profiles` / новые таблицы |
|
||||
|
||||
---
|
||||
|
||||
## 18. Связь с уведомлениями
|
||||
|
||||
| Аспект | Гость | Авторизованный пользователь |
|
||||
|---|---|---|
|
||||
| Контур уведомлений | G (`guest_notifications`) | P (`notifications.user_id`) |
|
||||
| Бейдж непрочитанных | нет | да |
|
||||
| Центр уведомлений | auth-gate | список |
|
||||
| Перенос G → P при логине | **запрещён** | — |
|
||||
|
||||
Домен User задаёт, **кто** видит контур P; домен Notification задаёт **что** показывается. Владелец персональных записей — всегда `user_identities.id`.
|
||||
Reference in New Issue
Block a user