# Бизнес-постановка: Уведомления (Notification Center) **Статус:** бизнес-постановка согласована (v3: D1–D13 закрыты) → вход в проектирование arch / OpenAPI / модули **Продукт:** HAN Chat (клиентское приложение + `api-backend`) **Источники:** макет Figma (**не канон** — только визуализация элементов UI, где применимо; при расхождении приоритет у этого ТЗ), черновик требований, архитектура `HAN_chat_specification` **Связанный backlog:** «Моделирование уведомлений»; смежно — кнопка «Позвонить оператору», кнопка «Войти»; **отдельно** — индикатор непрочитанных сообщений чата (не в scope этого ТЗ) --- ## 1. Цель Дать клиенту единый канал сервисных и маркетинговых коммуникаций вне чата: срочные напоминания, статусы услуг, документы, оплаты, акции, а также подсказки в гостевой зоне (авторизация, установка PWA). Чат остаётся каналом диалога с оператором; уведомления — канал «компания → клиент» с действиями (переход, оплата, загрузка/скачивание документов, переход в чат по акции). Непрочитанные ответы оператора в чате **не** моделируются как уведомления (см. §3.2). Гостевые `ads`/`promo` — не персонализированная реклама на клиента; отдельное маркетинговое согласие не требуется. Функционал описывается в Пользовательском соглашении. --- ## 2. Два контура показа | Контур | Кто видит | Источник данных | Frontend | |---|---|---|---| | **G. Гостевые уведомления** | Только гость | Таблица гостевых уведомлений в App DB → **public API** | Единственный источник в гостевом режиме | | **P. Персональные уведомления** | Только авторизованный | Таблица персональных (`user_id` NOT NULL) → **JWT API** | Единственный источник в ЛК | Правила: 1. До авторизации: только контур **G** (ответ public API as-is). 2. После авторизации: только контур **P**. Гостевые **не** показываются и **не** переносятся в персональные. 3. Смешивать G и P в одной таблице / через `user_id IS NULL` — **запрещено**. 4. В v1 **нет** клиентских условий показа (ОС / PWA / прочие фильтры): фронт показывает список из API в рамках лимита и сортировки. Локальная генерация карточек на устройстве **запрещена**. 5. У фронта всегда ровно один источник: public API **или** JWT API. Дубли тематики `ads`/`promo` в G и P допустимы; после входа виден только P. --- ## 3. Границы релиза ### 3.1. В scope - Контуры G и P (§2); персональный жизненный цикл: `lifecycle_status` / `visibility` / `is_read` / `close_reason` + `record_status` (arch-05). - UI: карусель на главной, Центр уведомлений, деталка (где применимо). - Каталог типов; публичный id — **UUID v7** (генерация в приложении). - Клиентские действия (ЛК): крестик, CTA, деталка/оплата, done/got it, документы (`chat.attachments.*`). - Internal API: **Create**, **Cancel** (без upsert / без update content) — §7.4. - Константы `notification.*` в `app_settings`; вложения — reuse `chat.attachments.*`. - Кнопки **Чат** + **Оператор** (`tel:` ← `operator.call.phone`). - Готовность модели к Web Push (UUID, события); реализация push — out of scope. ### 3.2. Вне scope - Тип `message` / непрочитанный чат (отдельная фича). - Push / Web Push / deep link из push. - Админ-UI для гостевых уведомлений (v1: insert/SQL). - Жёсткая привязка «тип ↔ upstream-сервис». - Профиль «Документы» / архив оплат — не заменяются уведомлениями. - Запись в `sync_queue` из application-кода: только **триггер БД** + новый `task_type`; куда в Bitrix — ТЗ `bitrix-sync`. - SMS/email поверх ЛК. - История чатов как UI-раздел — deprecated; backend API диалогов этим ТЗ не удаляется. - **Архив `lifecycle_status = N` в UI v1 — нет.** - Поле `opened` / impression — **не моделируем** (CTA → `is_read`). --- ## 4. Изменения UI | Место | Было | Стало | |---|---|---| | Под полем чата | (см. текущий UI) | **Чат** + **Оператор** (`operator.call.phone`) | | Верхнее меню | История чата | Центр уведомлений (колокольчик; бейдж только в ЛК) | | Главная | — | Карусель (лимит/сортировка §6.1) | | Центр | — | ЛК — список; гость — auth-gate + «Авторизоваться» | | Деталка | — | Для типов с деталкой; гостевые `ads`/`promo` — **без** деталки | Визуал типов — по Figma, где применимо. Figma не канон поведения. --- ## 5. Бизнес-модель ### 5.1. Слои контента 1. **Краткий** — баннер и строка списка: `header`, `text`, цена, CTA. 2. **Детальный** — `details.*`, документы, кнопки. Только если у типа есть деталка. ### 5.2. Справочник типов (**принято**) | Слой | Где | Содержимое | |---|---|---| | Структурный | App DB `notification_types` | `code`, `priority`, `countable`, признак «есть деталка», `record_status` | | Визуал | Frontend (тема/константы), сверка с Figma | Цвет, иконка, оформление | | Тексты по умолчанию | `text_resources` | `label`, дефолтный CTA, empty states | В экземпляре **не** дублируются label/цвет/icon/countable/priority/action — только `notification_type` (FK/code). ### 5.3. Каталог типов | type | label | priority | countable | Action по CTA | Контур | |---|---|---|---|---|---| | `authorize` | Гостевой режим | 2 | нет | Сценарий авторизации | G | | `install_app` | Приложение | 2 | нет | Адаптивная установка PWA (§5.7) | G | | `urgent` | Срочно | **1** | да | Детальный просмотр | P | | `reminder` | Напоминание | 2 | да | Детальный просмотр | P | | `news` | Новость | 2 | да | Детальный просмотр | P | | `ads` | Предложение | 2 | да* | G: чат после auth; P: чат + закрытие (§7.2) | G и/или P | | `promo` | Акция | 2 | да* | G: чат после auth; P: чат + закрытие (§7.2) | G и/или P | | `docs_required` | Требуются документы | 2 | да | Деталка (+ загрузка) | P | | `docs_ready` | Документы готовы | 2 | да | Деталка (+ скачивание) | P | | `status_changed` | Статус | 2 | да | Детальный просмотр | P | | `payment_pending` | Оплата | 2 | да | Переход по `payment_url` | P | \* Countable/read применяются только в контуре **P**. В **G** read-state нет. Типы `install_Android` / `install_IOS-HarmonyOS` / `message` — **отсутствуют**. `priority`: меньше = важнее. Seed: `urgent = 1`, остальные = `2`. CTA по умолчанию: - `authorize` — «Войти →» - `install_app` — «Установить →» (на UI инструкции текст может быть «Как установить →») - `urgent` / `reminder` / `news` / `status_changed` — «Подробнее →» - `ads` / `promo` — «Узнать подробнее →» - `docs_required` — «Загрузить документы →» - `docs_ready` — «Скачать →» - `payment_pending` — «Оплатить →» **Инвариант:** CTA у `docs_required` / `docs_ready` открывает **карточку**; upload/download — на карточке. **Инвариант:** жёсткой привязки «сервис X → тип Y» нет. ### 5.4. Поля экземпляра (контур P) | Поле | Обяз. | Описание | |---|---|---| | `id` | да | UUID v7 (api-backend при Create) | | `user_id` | да | `UserIdentity.id` NOT NULL | | `notification_type` | да | Код из справочника | | `notification_datetime` | да | UTC | | `header` | да | Заголовок | | `text` | нет | Подзаголовок | Опционально: | Поле | Описание | |---|---| | `date_expired` | → `lifecycle_status=N`, `close_reason=expired` | | `price` | Текущая цена (₽), опционально. Может быть без `old_price` | | `old_price` | Старая цена (₽), опционально. Имеет смысл только вместе с `price` (зачёркнутая «было») | | `payment_url` | Обязателен для `payment_pending` | | `details` / `details.*` | Деталка (если нужна) | | `details.deadline` | Срок на карточке | | `send_documents` | UI загрузки клиентом | | `documents[]` | Вложения компании в бакете **`han-chat-attachments`** (id объектов working S3) | | `chat_message_text` | Текст в чат при CTA (`ads`/`promo` P — **обязателен**) | | `button_done` / `button_gotit` / `button_gotit_text` | Кнопки деталки | | `external_id` + `source` | Идемпотентность Create (§8) | ### 5.5. Состояние экземпляра (контур P) | Атрибут | Значения | Семантика | |---|---|---| | `record_status` | `A` / `D` | Soft-delete строки (arch-05) | | `lifecycle_status` | `A` / `N` | `N` — нет в UI; в БД для аудита | | `visibility` | `V` / `I` | `I` — нет на главной, есть в Центре (пока `lifecycle_status=A`) | | `is_read` | да / нет | Для countable; бейдж = `A` ∧ countable ∧ ¬is_read | | `close_reason` | см. ниже | При `A` → `N` | | `close_reason` | Когда | |---|---| | `user_done` | «Выполнено»; успех upload `docs_required`; Cancel после успешной оплаты | | `expired` | `date_expired` | | `cancelled` | Cancel (отзыв системой) | **`is_read` ≠ `visibility`:** прочтение → бейдж; видимость → главная. Правила — §7.1. Контур G: per-user lifecycle / visibility / is_read **нет**. ### 5.6. Константы и мнемоники `notification.*` в `app_settings`. Вложения — ключи чата: - `chat.attachments.allowed_extensions` - `chat.attachments.allowed_mime_types` - `chat.attachments.disallowed_extensions` - `chat.attachments.max_size_mb` - `chat.attachments.presigned_upload_ttl_seconds` Оператор: **`operator.call.phone`**. Seed `notification.*`: | Ключ | Смысл | Default | |---|---|---| | `notification.gotit.default_ttl_days` | Got it без `date_expired` → now+N дней | `3` | | `notification.docs_ready.default_ttl_days` | После скачивания документов, если `date_expired` пуст → now+N дней | `3` | | `notification.home.max_items` | Лимит карусели | `15` | | `notification.center.max_items` | Лимит Центра (ЛК) | `15` | | `notification.carousel.autoplay_enabled` | `Y`/`N` | `N` | | `notification.carousel.autoplay_interval_ms` | Интервал | `5000` | Мнемоники UI (`text_resources`) — **принято**: - `notification.center.empty.title` / `.text` — пустой Центр (ЛК) - `notification.guest_center.title` / `.text` / `.cta` — гостевой Центр - `notification.detail.not_found.title` / `.text` — 404 деталки («На главную») API при чужом/`N`/отсутствии → **404**; UI — нейтрально «Уведомление недоступно». ### 5.7. Гостевые уведомления (контур G) - Одна таблица гостевых уведомлений; `record_status` + актуальность **`A`/`N`** (без per-user state). - Public API отдаёт все активные (`A`); фронт **не фильтрует** по ОС/PWA в v1. - Наполнение v1 — insert/SQL; public API — read-only. - Типы в G: `authorize`, `install_app`, `ads`, `promo` (и при необходимости другие broadcast-типы позже). - Крестика нет. Деталки у `ads`/`promo` в G **нет**. **`install_app` — один тип, адаптивный CTA (принято, технически возможно):** 1. Пользователь нажал CTA. 2. Если среда поддерживает установку PWA (`beforeinstallprompt` / эквивалент) — запускаем установку. 3. Иначе — показываем инструкцию установки (локальный экран/модалка на фронте; отдельный тип в API не нужен). Fallback (если адаптивный CTA окажется нереализуем на конкретной платформе сборки): две записи в G (`install` для Android-потока и для iOS/Harmony) — только как запасной план реализации, не целевая модель данных. **CTA прочих G:** - `authorize` → сценарий авторизации. - `ads` / `promo` → как «популярные вопросы»: старт auth; после успеха в чат уходит `chat_message_text` кампании от лица клиента. Кампания для других гостей остаётся `A`. ### 5.8. Персональные (контур P) — кратко Создаются Internal **Create**; полная модель §5.4–5.5. Поведение CTA — §7. --- ## 6. Поведение UI ### 6.1. Главная (карусель) **ЛК (P):** - Выборка: `record_status=A`, `lifecycle_status=A`, `visibility=V`. - Лимит `notification.home.max_items` (15). - Сортировка: `priority` ↑, затем `notification_datetime` ↓. - Крестик → `visibility=I` (синхрон на все устройства). - Карусель: свайп; автопрокрутка по settings. - CTA → action типа + эффекты §7.1. **Гость (G):** - Список = ответ public API; лимит 15; сортировка: `priority` ↑, затем datetime ↓ (поля сортировки — в таблице G / типе). - Крестика нет. Один источник — public API. ### 6.2. Центр **Гость:** колокольчик без бейджа; экран auth-gate + «Авторизоваться» (мнемоники §5.6). Списка нет. **ЛК:** - `record_status=A`, `lifecycle_status=A` (в т.ч. `visibility=I`), лимит 15, без архива `N`. - Точка на непрочитанных countable. - Сортировка: `priority` ↑ → непрочитанные выше → datetime ↓. - Бейдж = непрочитанные countable (`lifecycle_status=A`); sync через бэкенд. - Empty state — мнемоники §5.6. ### 6.3. Деталка (контур P; инструкция `install_app` — локальный UI) - SPA: `/notification/{uuid}`. - Чужой / `N` / deleted → **404** (не 403). - Upload: quarantine → Message Safety → working (`han-chat-attachments`) → **триггер БД** → `sync_queue`. - Company `documents[]`: тот же бакет **`han-chat-attachments`**; preview/download + audit как в чате. - `button_done` → `lifecycle_status=N`, `close_reason=user_done`. - `button_gotit` → `visibility=I`; при пустом `date_expired` — now + `notification.gotit.default_ttl_days`; `is_read=true` (got it = действие пользователя, см. §7.1). - Обе кнопки допустимы одновременно; исход по нажатой. Обе false → только back. ### 6.4. Завершение по типам (P) | Тип | → `lifecycle_status=N` | |---|---| | `payment_pending` | Cancel после callback оплаты, `close_reason=user_done` | | `docs_required` | Успешный upload → `user_done` | | Другой тип с `send_documents` | Upload сам не закрывает | | `ads` / `promo` | CTA → `N` (§7.2) | | `status_changed` | «Выполнено»; с главной уходит уже по CTA (`visibility=I`) | | `docs_ready` | После скачивания — `I` + TTL/`date_expired` (§7.1); в `N` уходит по expire (или Cancel/done) | | Прочие | done / expired / cancelled | --- ## 7. Матрицы поведения (контур P) ### 7.1. `is_read` и `visibility` — раздельно **Правило прочтения (единое):** > Любое **CTA** → `is_read = true` (идемпотентно). CTA = первичное действие карточки: кнопка CTA на баннере, тап по строке Центра ведущий к action типа, «Оплатить», переход в деталку по CTA, отправка в чат по ads/promo. Не CTA: системный back, крестик (крестик не ставит `is_read`). **Матрица `visibility` / `lifecycle` (избирательно):** | Тип | Событие | `is_read` | `visibility` | `lifecycle_status` | |---|---|---|---|---| | любой | Крестик на главной | — | `I` | — | | любой с `button_gotit` | Got it | `true` | `I` (+ TTL если нужно) | — | | любой с `button_done` | «Выполнено» | `true` | — | `N` / `user_done` | | `urgent` | CTA → деталка | `true` | — | — | | `reminder` | CTA → деталка | `true` | — | — | | `status_changed` | CTA → деталка | `true` | `I` | — | | `news` | CTA → деталка | `true` | `I` | — | | `ads` / `promo` | CTA (чат) | `true` | `I` | `N` (+ сообщение в чат) | | `docs_required` | CTA → деталка | `true` | — | — | | `docs_required` | Успешный upload | `true` | — | `N` / `user_done` | | `docs_ready` | CTA → деталка | `true` | — | — | | `docs_ready` | Скачивание документов | `true` | `I` | — (+ TTL, см. ниже) | | `payment_pending` | CTA «Оплатить» | `true` | `I` | — (далее Cancel→`N` при оплате) | Пояснения: - Прочерк «—» = поле этим событием не меняется. - Для `ads`/`promo`: одновременно `is_read`, `visibility=I` и `lifecycle_status=N`. В UI запись исчезает из-за `N`; `I` фиксирует намерение «убрать с главной» на случай гонок/аудита. - Открытие деталки из Центра по тапу строки = CTA → всегда `is_read=true`; `visibility` меняется только если тип/событие есть в матрице выше. - `status_changed`: CTA → деталка сразу уводит карточку с главной (`visibility=I`); в Центре остаётся, пока `lifecycle_status=A`. - `docs_ready`, скачивание документов: `is_read=true`, `visibility=I`; если `date_expired` пуст — выставить `date_expired = now + notification.docs_ready.default_ttl_days` (default 3). Если `date_expired` уже задан продюсером — **не перезаписывать**. Далее сработает общий expired-job → `lifecycle_status=N`, `close_reason=expired`. ### 7.2. `ads` / `promo` (P) 1. CTA отправляет в чат `chat_message_text` (обязательное поле). 2. `is_read=true`, `visibility=I`, `lifecycle_status=N`. 3. Деталки у персональных ads/promo на MVP **нет** (симметрично G). ### 7.3. Валидация Internal Create (**принято**) | Тип | Обязательно | Не ожидается | Примечания | |---|---|---|---| | `urgent`, `reminder`, `news`, `status_changed` | `header`; `details` если нужна деталка | `payment_url` | `button_*` по необходимости | | `ads`, `promo` | `header`; **`chat_message_text`** | `payment_url` | `price` и `old_price` — разные опц. поля; `price` без `old_price` допустим; `old_price` без `price` — нет; деталки нет | | `docs_required` | `header`; `send_documents=true` | `payment_url` | | | `docs_ready` | `header`; `documents[]` ≥1 | `payment_url` | | | `payment_pending` | `header`; `payment_url` | — | `price` **не** обязателен; `details` **разрешены** | | любой | `user_id`, `notification_type`, `source`, `external_id` | неизвестный тип / user | | Ошибка валидации → `400` (OpenAPI). Типы контура G через Internal Create **не** создаются (только insert/public table). ### 7.4. Контракт продюсера | Операция | Семантика | |---|---| | **Create** | Создать новое уведомление. Ключ `source`+`external_id`: если уже есть активная (`lifecycle_status=A`) запись с той же парой → **409 Conflict** (контент не обновляется). | | **Cancel** | `lifecycle_status=N` + `close_reason` (`cancelled` \| `user_done`). | | **Смена контента** | Upsert / update content **нет**. Сценарий: **Cancel** старого → **Create** нового (новый `external_id` или та же пара после `N` — допустим Create, т.к. активной записи с ключом больше нет; политика ключа после `N` — в OpenAPI: разрешить Create с тем же ключом только если нет активной). | Кто закрывает: | Сценарий | Кто | reason | |---|---|---| | Оплата успешна | Внешний → Cancel | `user_done` | | Upload `docs_required` | api-backend | `user_done` | | CTA ads/promo | api-backend по действию клиента | (закрытие как `user_done` или отдельный reason — по OpenAPI; default `user_done`) | | «Выполнено» | Клиент | `user_done` | | Отзыв | Внешний → Cancel | `cancelled` | | TTL | Scheduler | `expired` | --- ## 8. Идентификация - Id P и G — **UUID v7** (генерация приложением / seed-скриптом, не клиентом). - SPA: `/notification/{uuid}` (только P; гостевая инструкция install — локальный роут без обязательного id в API). - Deep link — с push (later). - Идемпотентность Create — §7.4 (без upsert). --- ## 9. API ### 9.1. Клиентские (JWT) — контур P Список / get one / hide / mark-read·done·gotit / upload / counter — по §6–§7. Владелец = `user_id` из JWT. Чужое/`N` → 404. ### 9.2. Public — контур G `GET` списка активных гостевых уведомлений (без JWT). ### 9.3. Internal - Path: **`/internal/notifications/v1/...`** - Service token — как у прочих internal (arch-02). - Callers: сервисы приватной сети облака (в т.ч. другие ВМ). Интернет — нельзя. - Операции: **Create**, **Cancel** только. ### 9.4. Realtime (**подписка**) Желательно в релизе, иначе polling. После connect на `WS /api/v1/realtime` клиент выполняет явный **`subscribe` на уведомления** (например `subscribe: { notifications: true }`), отдельно от `dialog_ids`. События: `notification.created` / `notification.updated` / `notification.closed`. Reconnect → reconciliation GET списком. Гостю WS для G не требуется. ### 9.5. Готовность к Web Push Стабильный UUID v7; события жизненного цикла; `header`/`text` для текста push; path `/notification/{uuid}`. Реализация push — вне scope. --- ## 10. Смежные сервисы | Компонент | Изменение | |---|---| | **Frontend** | Один источник (G или P); карусель; Центр; адаптивный `install_app`; Чат/Оператор | | **api-backend** | Модель G+P; public/JWT/internal; scheduler expired; WS subscribe notifications; upload | | **App DB** | Две таблицы; types+priority; триггер → `sync_queue`; seed | | **nginx** | Internal не в интернет; rate limit; SPA | | **Redis/WS** | События notification.* по подписке | | **message-safety** | Как chat attachments | | **bitrix-sync** | Новый task_type | | **S3** | Бакет вложений уведомлений (клиент и компания): **`han-chat-attachments`** | | **settings** | `notification.*`, `chat.attachments.*`, `operator.call.phone`, мнемоники | Порядок: (1) модель + API + UI → (2) Internal Create/Cancel + seed G → (3) документы + триггер → (4) WS subscribe. --- ## 11. Критерии приёмки 1. Колокольчик; ЛК — бейдж непрочитанных, sync `is_read`/`visibility` между устройствами; гость — без бейджа, Центр = «Авторизоваться». 2. ЛК главная: лимит 15, sort priority→datetime, только `A`+`V`; крестик → `I`; `N` не в UI. 3. Гость: только public API, без клиентских фильтров ОС/PWA; один тип `install_app` с адаптивным CTA. 4. Типы каталога отличимы; `message` нет; после логина только P. 5. Деталка/кнопки по флагам; docs_* CTA → карточка; ads/promo P: чат + `is_read`+`I`+`N`. 6. Internal только Create/Cancel; дубликат активного `source`+`external_id` → 409; смена контента = Cancel+Create. 7. Expired → `N`, UI исчезает. 8. Upload → quarantine → working → триггер → `sync_queue`; company docs в `han-chat-attachments`. 9. Оператор — только `operator.call.phone`. 10. Константы из `app_settings`; UUID v7; 404 на чужое/`N`. 11. Валидация Create по §7.3. 12. WS: события после `subscribe` на notifications. 13. Push не обязателен; модель push-ready. 14. Архива `N` нет; поля `opened` нет. --- ## 12. Решения D1–D13 (закрыты) | # | Решение | |---|---| | **D1** | Два контура: **G** (всё через public API) и **P** (JWT). Без локальных карточек и без условий показа в v1. `install_app` — один тип, адаптивный CTA (установка / инструкция). Fallback — два баннера в данных, не целевая модель. | | **D2** | Схема справочника §5.2 принята. | | **D3** | Мнемоники empty/404 и поведение 404 приняты (§5.6). | | **D4** | ads/promo CTA: `is_read` + `visibility=I` + `lifecycle_status=N` + чат. | | **D5** | Любое CTA → `is_read=true`. `visibility` — только по матрице §7.1. | | **D6** | Валидация §7.3: `chat_message_text` обязателен для ads/promo; `price` у payment необязателен; `details` у payment разрешены. | | **D7** | `/internal/notifications/v1/...` + service token. | | **D8** | WS: явная подписка на notifications. | | **D9** | Бакет company/client файлов уведомлений: `han-chat-attachments`. | | **D10** | `opened` / impression удалены; достаточно `is_read` от CTA. | | **D11** | Upsert нет; механика **Cancel + Create**; конфликт активного ключа → 409. | | **D12** | IDOR: единый 404. | | **D13** | Одновременные done+gotit допустимы без доп. правил продюсеру. | --- ## 13. История решений (Q) | # | Решение | |---|---| | **Q1** | Непрочитанный чат / `message` — вне ТЗ. | | **Q2** | `N` в БД, не в UI; архива v1 нет. | | **Q3** | `payment_pending`: CTA «Оплатить» → `is_read`+`I`; деталка не обязательна. | | **Q4** | Гостевые — отдельная таблица; insert v1. | | **Q5** | Документы через S3; клиент quarantine→working. | | **Q6** | `sync_queue` только триггером БД. | | **Q7** | Лимит 15; sort priority→datetime. | | **Q8** | Подсказки установки/входа — в контуре G через API (не локальный FE). | | **Q9** | UUID v7; `/notification/{uuid}`. | | **Q10** | `message` исключён. | | **Q11** | Владелец = `user_id`. | | **Q12** | `record_status` ≠ `lifecycle_status`. | | **Q13** | `operator.call.phone`. | | **Q14** | Got it = `I` (+TTL), не обязательно `N`. | | **Q15** | Два контура G/P (вместо A/B/C). | | **Q16** | Гостевой Центр = auth-gate. | | **Q17** | Reuse `chat.attachments.*`. | | **Q18** | Поле `opened` снято (D10). | | **Q19** | Модель push-ready; push вне scope. | ---