31 KiB
Бизнес-постановка: Уведомления (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 |
Единственный источник в ЛК |
Правила:
- До авторизации: только контур G (ответ public API as-is).
- После авторизации: только контур P. Гостевые не показываются и не переносятся в персональные.
- Смешивать G и P в одной таблице / через
user_id IS NULL— запрещено. - В v1 нет клиентских условий показа (ОС / PWA / прочие фильтры): фронт показывает список из API в рамках лимита и сортировки. Локальная генерация карточек на устройстве запрещена.
- У фронта всегда ровно один источник: 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; вложения — reusechat.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. Слои контента
- Краткий — баннер и строка списка:
header,text, цена, CTA. - Детальный —
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_extensionschat.attachments.allowed_mime_typeschat.attachments.disallowed_extensionschat.attachments.max_size_mbchat.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 (принято, технически возможно):
- Пользователь нажал CTA.
- Если среда поддерживает установку PWA (
beforeinstallprompt/ эквивалент) — запускаем установку. - Иначе — показываем инструкцию установки (локальный экран/модалка на фронте; отдельный тип в 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)
- CTA отправляет в чат
chat_message_text(обязательное поле). is_read=true,visibility=I,lifecycle_status=N.- Деталки у персональных 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. Критерии приёмки
- Колокольчик; ЛК — бейдж непрочитанных, sync
is_read/visibilityмежду устройствами; гость — без бейджа, Центр = «Авторизоваться». - ЛК главная: лимит 15, sort priority→datetime, только
A+V; крестик →I;Nне в UI. - Гость: только public API, без клиентских фильтров ОС/PWA; один тип
install_appс адаптивным CTA. - Типы каталога отличимы;
messageнет; после логина только P. - Деталка/кнопки по флагам; docs_* CTA → карточка; ads/promo P: чат +
is_read+I+N. - Internal только Create/Cancel; дубликат активного
source+external_id→ 409; смена контента = Cancel+Create. - Expired →
N, UI исчезает. - Upload → quarantine → working → триггер →
sync_queue; company docs вhan-chat-attachments. - Оператор — только
operator.call.phone. - Константы из
app_settings; UUID v7; 404 на чужое/N. - Валидация Create по §7.3.
- WS: события после
subscribeна notifications. - Push не обязателен; модель push-ready.
- Архива
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. |