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

555 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Бизнес-постановка: Пользователь (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 + `bitrix_contact_id` | UI-поля — последнее успешно синхронизированное значение (входящий поток MVP — Bitrix24); auth-телефон инициирует sync, но master телефона — Keycloak |
| **Bitrix Contact** | CRM Bitrix24 | Карточка клиента в CRM | Bitrix24 для CRM-полей; связь через `bitrix_contact_id` / `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, foreign_phone | Последний успешный sync (MVP: Bitrix → App) | `client_profiles` |
| `bitrix_contact_id` | Результат `bitrix-sync` | `client_profiles` |
| 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` | Аналитическая сессия |
| `bitrix_contact_id` | Contact в CRM (после 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 |
| `bitrix_contact_id` | varchar/nullable | После успешного map |
| `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 | обязательны | |
Partial unique на `bitrix_contact_id` среди active. 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` | `bitrix-sync` | После bootstrap / изменения профиля |
| 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, доля пользователей без `bitrix_contact_id` спустя N минут после входа.
---
## 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`.