Добавлены уведомления

This commit is contained in:
mi
2026-07-27 17:36:53 +03:00
parent a072005164
commit 958fba5f3e
149 changed files with 6371 additions and 110 deletions
+16
View File
@@ -33,6 +33,11 @@
| `app_settings` | `han_app` | Бизнес-настройки |
| `text_resources` | `han_app` | Тексты UI по мнемоникам |
| `popular_questions` | `han_app` | Популярные вопросы главного экрана |
| `Notification` / `GuestNotification` | `han_app` | Персональное уведомление / общая гостевая кампания |
| `NotificationType` | `han_app` | Вид уведомления как данные: контур, приоритет, CTA, кнопки и оформление |
| `NotificationCtaAction` / `NotificationButton` / `NotificationColorToken` | `han_app` | Реестры реализованных механик CTA, кнопок и семантической палитры |
| `NotificationSource` | `han_app` | Продюсер Internal Notifications API; хранит hash индивидуального токена, не секрет |
| `ClientDocument` | `han_app` | Отправленный клиентом проверенный документ; создаёт `document.client_uploaded` в `sync_queue` |
| `dialog_sessions` | `bitrix_local` | Маппинг чата Open Lines |
| `sms_template` | `sms` | Версионируемый согласованный SMS-шаблон; active-версия уникальна для `code`+`channel`+`locale` |
| `sms_setting` | `sms` | Технические runtime-настройки `sms-service`, не секреты и не OTP product settings |
@@ -54,6 +59,8 @@
| `task_id` | ID async-проверки Message Safety |
| `request_id` | Корреляция HTTP-запроса (заголовок `X-Request-ID`) |
| `sms_message_id` | UUID `sms.sms_outbound_message.id`; логическая ссылка из Keycloak challenge/event, межсхемного FK нет |
| `notification_id` | UUID v7 персонального или гостевого уведомления; генерирует приложение/seed |
| `external_id` уведомления | Бессрочный бизнес-ключ продюсера в паре с `source` |
| `provider_message_id` | `messageUuid` i-Digital Direct; хранится только в `sms-service` |
| `provider_external_id` | `externalMessageId`; в v1 равен `sms_message_id` и является корреляцией, а не доказанной идемпотентностью Direct |
@@ -154,6 +161,15 @@ Realtime-событие `message.status` передаёт актуальные `
| `sync` | `bitrix-sync` |
| `settings` | internal settings bridge на `api-backend` для Keycloak SPI |
| `sms` | `sms-service`; durable order/read API во внутренней сети |
| `notifications` | Internal Create/Cancel уведомлений на `api-backend`; токен отдельный для каждого `source` |
## Жизненный цикл уведомления
- `lifecycle_status`: `active` / `closed`; бизнес-завершение, не soft delete.
- `visibility`: `visible` / `hidden`; скрытое персональное уведомление отсутствует на главной, но остаётся в Центре, пока активно.
- `close_reason`: `user_done`, `docs_submitted`, `offer_accepted`, `paid`, `expired`, `cancelled`.
- `record_status='D'` означает только административное удаление ошибочной записи и не заменяет `lifecycle_status`.
- `cta_action` и эффекты кнопок определяются справочниками; новый вид на существующих механиках добавляется данными.
## SMS-конфигурация
+10 -3
View File
@@ -21,6 +21,8 @@ HAN Chat - приложение для мигрантов, где стартов
- Вложения чата MVP: **только изображения и PDF** — см. [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), «Разрешённые типы файлов чата».
- SMS OTP вводится поэтапно: до production rollout действует явный mock (`KEYCLOAK_OTP_MOCK_ENABLED=true`); целевой real mode — Keycloak генерирует/локально проверяет OTP и создаёт durable order в `sms-service`, а worker асинхронно вызывает i-Digital Direct. Контракт и gates — [`module-11-idgtl-sms.md`](../modules/module-11-idgtl-sms.md).
- Популярный вопрос при выборе **автоматически отправляется как сообщение**; если пользователь не авторизован — сначала согласия и OTP, затем отправка.
- Notification Center v1 использует два контура: G — общие read-only гостевые кампании, P — персональные уведомления с состоянием в App DB. Виды, CTA, кнопки и палитра задаются каталогом данных.
- Инструкция `install_app` всегда открывается во внешней новой вкладке; iframe/модалка для неё не используется.
- Перечень таблиц и миграций App DB проектирует модуль `database` (и владельцы схем других сервисов); arch фиксирует только **разделение схем** PostgreSQL и контракты между сервисами.
## Пользовательские сценарии
@@ -44,6 +46,7 @@ HAN Chat - приложение для мигрантов, где стартов
- Keycloak: identity provider, OTP-only авторизация по номеру телефона.
- SMS Service: internal durable order API, шаблоны и бессрочный журнал SMS; отдельный worker вызывает i-Digital Direct, callback обновляет только журнал.
- api-backend: Python-приложение с REST API, realtime-доставкой сообщений и бизнес-логикой.
- Notification producers: сервисы приватной сети, создающие/отменяющие персональные уведомления через Internal API с отдельным Bearer token на `source`; `producer_test` используется только для smoke API.
- Nginx Reverse Proxy: единая публичная точка входа, HTTPS termination и маршрутизация на Keycloak/API/frontend web/Bitrix24.
- Message Safety Service: отдельный сервис проверки входящих сообщений; вызов из API → `200 allow` | `403 deny` | `203 pending` + `task_id` (при `203` api-backend синхронно поллит task до финального вердикта, без очереди анализа на api-backend).
- Bitrix24 Local App: локальное приложение, custom connector `han_mobile_app` для Bitrix24 Open Lines: чат, OAuth, webhook-события, маппинг `dialog_id``bitrix_chat_id`.
@@ -186,6 +189,9 @@ Frontend не должен:
- circuit breaker + timeout budget на вызовы `message-safety` и `bitrix-local-app` (I2);
- auth-aware rate limits для сообщений, пользовательских и сервисных операций;
- аудит пользовательских действий;
- публичный каталог/гостевые кампании, JWT API Notification Center и Internal Create/Cancel; дедупликацию по бессрочной паре `(source, external_id)`;
- применение каталога уведомлений без ветвления по `notification_type`, пользовательские действия, документы и события `notification.created|updated|closed`;
- expire job и очистку upload drafts. При скрытии TTL задаёт `date_expired` только если оно отсутствует; существующая дата не меняется;
- единые ошибки и валидацию входных данных.
### Bitrix24 Local App
@@ -458,11 +464,11 @@ api-backend не решает, sync или async нужна проверка в
9. Frontend отображает сообщение оператора в чате.
10. При получении от `bitrix-local-app` доменного события `dialog.closed` (Bitrix24 `ONIMCONNECTORDIALOGFINISH`) API переводит `Dialog.status` в `closed`.
## Документы компании (post-MVP)
## Документы компании
Доставка документов из Bitrix24 в приложение **не входит в MVP** — см. [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 9.
Notification Center v1 регистрирует переданные продюсером объекты `han-chat-documents` в реестре `documents` и связывает их с уведомлением. Это первый действующий канал наполнения будущего общего блока профиля; доставка из Bitrix24 остаётся вне scope.
В MVP блок профиля «Документы» и API `GET /api/v1/me/documents` зарезервированы; список может быть пустым. Контракт endpoint — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md).
Скачивание выполняется owner-only по короткому presigned GET с audit. Для вида с `hide_on_document_download=true` первое скачивание **любого** связанного документа атомарно скрывает уведомление; последующие скачивания не меняют состояние. Если `date_expired` уже задано, оно сохраняется; TTL скрытия устанавливает дату только при её отсутствии.
## Профиль клиента
@@ -519,6 +525,7 @@ App DB — **локальный кэш** для UI. Двусторонний syn
- Путь входит в `/api/*`; отдельный location `/realtime/*` в nginx **не** нужен.
- Fallback: polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
- События: новое сообщение, смена `delivery_status` / `safety_status`, смена `Dialog.status`.
- Подписка расширена опциональным `notifications` (default `false`); канал пользователя передаёт `notification.created`, `notification.updated`, `notification.closed`, включая эхо инициатору. После reconnect источник истины — REST.
## Принципы безопасности
+36 -3
View File
@@ -30,6 +30,7 @@
| `BITRIX_SYNC_SERVICE_TOKEN` | `bitrix-sync` | ops / мониторинг | `GET /internal/sync/v1/*` | `Authorization: Bearer` или `X-Service-Token` |
| `KEYCLOAK_SETTINGS_BRIDGE_TOKEN` | `api-backend` | Keycloak SPI | `GET /internal/settings/v1/otp` | `Authorization: Bearer` |
| `SMS_SERVICE_TOKEN` | `sms-service` | Keycloak SPI | `POST/GET /internal/sms/v1/*` | `Authorization: Bearer` |
| `NOTIFICATIONS_TOKEN_<SOURCE>` | `api-backend` | соответствующий продюсер | `POST /internal/notifications/v1/*` | `Authorization: Bearer` |
Пары значений (должны совпадать):
@@ -39,6 +40,8 @@
Генерация: `openssl rand -hex 32`. Секреты не коммитить.
Для Notifications токен отдельный на каждый `source`: секрет существует только в deployment secret/env, а `notification_sources` хранит только hash. Токен разрешает identity продюсера и сравнивается constant-time; `source` в body обязан совпасть. Seed-источник `producer_test` и `NOTIFICATIONS_TOKEN_PRODUCER_TEST` предназначены для smoke Create/Cancel, не для бизнес-интеграции.
**Не путать с webhook-токенами** (публичные callback от Bitrix24, не internal service API):
| Переменная | Назначение |
@@ -68,6 +71,15 @@
| `POST /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/complete` | `api-backend` | Expo frontend | Подтверждение загрузки, проверка объекта в quarantine, фиксация checksum/metadata | JWT |
| `GET /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/download-url` | `api-backend` | Expo frontend | Presigned URL вложения чата; обязателен audit | JWT |
| `WS /api/v1/realtime` | `api-backend` | Expo frontend | Realtime-события чата, статусы доставки, unread | JWT |
| `GET /api/v1/public/notifications` | `api-backend` | Expo frontend | Активные гостевые кампании G | public + CORS/rate limit |
| `GET /api/v1/public/notification-types` | `api-backend` | Expo frontend | Публичный каталог видов с ETag, без серверных правил переходов | public + cache/rate limit |
| `GET /api/v1/notifications?place=home\|center` | `api-backend` | Expo frontend | Персональная выборка P с серверными лимитами 7/15 и сортировкой | JWT |
| `GET /api/v1/notifications/counter` | `api-backend` | Expo frontend | Счётчик непрочитанных в окне Центра | JWT |
| `GET /api/v1/notifications/{id}` | `api-backend` | Expo frontend | Деталка активного собственного уведомления | JWT |
| `POST /api/v1/notifications/{id}/read|hide|cta` | `api-backend` | Expo frontend | Идемпотентные действия и CTA по каталогу | JWT + rate limit |
| `POST /api/v1/notifications/{id}/buttons/{button_code}` | `api-backend` | Expo frontend | Единое действие кнопки деталки | JWT + rate limit |
| `GET /api/v1/notifications/{id}/documents/{document_id}/download-url` | `api-backend` | Expo frontend | Presigned GET + audit; первое скачивание любого связанного документа может скрыть уведомление | JWT |
| `POST/GET/DELETE /api/v1/uploads/*` | `api-backend` | Expo frontend | Универсальные upload drafts клиента | JWT + rate limit |
Единый формат ошибки:
@@ -101,7 +113,10 @@
| `403` | `forbidden` | Доступ запрещён и ресурс не скрывается | нет |
| `404` | `not_found` | Ресурс не существует или принадлежит другому пользователю | нет |
| `409` | `idempotency_key_reused` | Тот же `Idempotency-Key` с другим fingerprint | нет |
| `409` | `notification_conflict` | `(source, external_id)` уже занят Create с другим fingerprint | нет |
| `409` | `notification_closed` | Действие по уже закрытому уведомлению | нет |
| `422` | `message_blocked` | Message Safety вернул final deny | нет |
| `422` | `button_not_allowed` | Кнопка не привязана к виду уведомления | нет |
| `429` | `rate_limit_exceeded` | Edge/API лимит превышен; должен быть `Retry-After`, если повтор допустим | да |
| `503` | `dependency_unavailable` | Circuit open или недоступны safety/Bitrix/S3 | да |
| `504` | `dependency_timeout` | Истёк timeout budget внешней зависимости | да |
@@ -313,10 +328,10 @@ Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` и
3. Клиент отправляет подписку:
```json
{ "type": "subscribe", "dialog_ids": ["uuid"] }
{ "type": "subscribe", "dialog_ids": ["uuid"], "notifications": true }
```
4. Сервер отвечает `{ "type": "subscribed", "dialog_ids": ["uuid"] }`.
4. Сервер отвечает `{ "type": "subscribed", "dialog_ids": ["uuid"], "notifications": true }`. Поле `notifications` опционально, default `false`; старые chat-клиенты совместимы.
**События сервер → клиент:**
@@ -325,12 +340,17 @@ Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` и
| `message.new` | Новое сообщение в диалоге | `dialog_id`, `message` (DTO как в REST) |
| `message.status` | Смена `safety_status` / `delivery_status` | `dialog_id`, `message_id`, `safety_status`, `delivery_status` |
| `dialog.status` | Смена `Dialog.status` | `dialog_id`, `status` |
| `notification.created` | Создано персональное уведомление | `event_id`, `occurred_at`, `notification`, `unread_count` |
| `notification.updated` | Изменено состояние/документы уведомления | `event_id`, `occurred_at`, `notification_id`, изменённые поля, `unread_count` |
| `notification.closed` | Уведомление закрыто | `event_id`, `occurred_at`, `notification_id`, `close_reason`, `unread_count` |
**Reconnect:**
- exponential backoff: 1s → 2s → 4s → … max 30s;
- после reconnect — повтор `subscribe` с актуальным списком `dialog_ids`;
- при недоступности WS > 30s — fallback на polling `GET .../messages?after=<cursor>`.
- при недоступности WS > 30s — fallback на polling чата и, при подписке на уведомления, `GET /api/v1/notifications` + `/counter` раз в 60 секунд.
События уведомлений публикуются в `han:rt:user:{user_id}` на все соединения, включая инициатора. Массовый expire job не отправляет событие на каждую запись; reconnect/polling всегда выполняет REST reconcile.
**Ping:** сервер может слать `{ "type": "ping" }` каждые 30s; клиент отвечает `{ "type": "pong" }`.
@@ -350,6 +370,19 @@ Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` и
- internal API версионируется тем же правилом (`/internal/{mnemonic}/v2/...`);
- OpenAPI генерируется или поддерживается вручную — на усмотрение модуля, но файл обязателен в DoD (arch-05).
## Producers ↔ api-backend: Notifications
| Контракт | Назначение | Защита |
|---|---|---|
| `POST /internal/notifications/v1/notifications` | Create персонального уведомления | private network + Bearer token конкретного `source` |
| `POST /internal/notifications/v1/notifications/cancel` | Cancel по `(source, external_id)` с `cancelled` или `paid` | то же |
Пара `(source, external_id)` уникальна бессрочно и заменяет `Idempotency-Key`: одинаковый canonical fingerprint возвращает существующую запись с `200`, другой — `409 notification_conflict`. Cancel идемпотентен; чужой `source` не раскрывается.
Каталог, валидация `details`, обязательных полей CTA и эффектов кнопок применяются по данным справочников без ветвления по `notification_type`. Инструкция `install_app` всегда возвращает открытие `instruction_url` в новой вкладке, без iframe/модалки.
При скрытии действие всегда ставит `visibility=hidden`. TTL из вида/default применяется только если `date_expired IS NULL`; уже заданная продюсером дата сохраняется. Первое успешное получение download URL для **любого** связанного документа считается началом скачивания и, при `hide_on_document_download=true`, один раз скрывает уведомление; последующие документы состояние не меняют.
## Keycloak SPI ↔ api-backend settings bridge
Keycloak SPI получает product limits OTP из `app_settings` через internal endpoint, а не через прямой доступ к `han_app`.
@@ -23,7 +23,7 @@
- `/bitrix/sync/*` (public: webhook CRM sync для `bitrix-sync`) → `bitrix-sync`;
- exact `POST /callbacks/idgtl/sms``sms-service`; остальные методы и SMS paths не публикуются;
- web-сборка frontend или прокси на dev-сервер;
- `/internal/openlines/*`, `/internal/safety/*`, `/internal/sync/*`, `/internal/sms/*` **не публикуются** наружу — доступны только из внутренней Docker-сети.
- `/internal/openlines/*`, `/internal/safety/*`, `/internal/sync/*`, `/internal/sms/*`, `/internal/notifications/*` **не публикуются** наружу — доступны только из внутренней Docker-сети.
- Никакой другой `nginx` (ни в контейнере сервиса, ни на хосте) не терминирует внешний HTTPS для backend-контура. Site-конфиг `tohin.ru` на хосте, если используется, должен проксировать весь трафик на корневой `nginx` контейнера, а не на порты отдельных сервисов напрямую.
### Структура compose через `include`
@@ -158,6 +158,8 @@ Python FastAPI backend.
- принимает forward нормализованных событий оператора от `bitrix-local-app`;
- поддерживает realtime endpoint для сообщений оператора;
- работает с Selectel S3 для файлов и документов;
- обслуживает Notification Center G/P и Internal Create/Cancel; token каждого продюсера передаётся через env/secret, в App DB хранится только hash;
- имеет отдельные процессы expire job (ежедневно 00:01 UTC, advisory lock) и cleanup upload drafts/S3; они используют тот же immutable image и не публикуют порты;
- экспортирует traces/logs в `otel-collector`;
- не хранит состояние внутри контейнера.
@@ -300,6 +302,8 @@ Identity provider. **Обязателен** в compose-контуре с пер
Корневой `backend/.env` читается всеми сервисами compose через `${VAR}` в сервисных `docker-compose.yml`. Канонический `.env.example` и `app_settings` — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md); контракты service tokens — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md).
Для smoke-продюсера обязателен `NOTIFICATIONS_TOKEN_PRODUCER_TEST`; это secret, а не `app_settings`. Compose передаёт его только `api-backend` и notification workers. Зарегистрированные entrypoints: `han-notification-expire-worker` и `han-notification-draft-cleanup-worker`; выдуманный command без project script в deployment запрещён.
## HTTPS и TLS
Соответствует [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Принципы безопасности» (HTTPS, TLS, HSTS). Инфраструктурная реализация:
@@ -330,6 +334,7 @@ Identity provider. **Обязателен** в compose-контуре с пер
- слабые шифры запрещены на уровне `nginx`;
- `nginx` скрывает `Server`, `X-Powered-By` и аналогичные технологические заголовки;
- security headers: `Strict-Transport-Security`, `X-Content-Type-Options`, `Referrer-Policy`, `Content-Security-Policy` для web-приложения;
- инструкция по установке всегда открывается новой вкладкой, поэтому CSP SPA задаёт `frame-src 'none'`; allow-list iframe для инструкций отсутствует;
- секретный ключ сертификата не коммитится в репозиторий;
- использовать сертификаты доверенного CA; автоматизировать выпуск и продление (Let's Encrypt + reload `nginx`);
- закрыть прямой доступ к внутренним портам контейнеров извне.
@@ -367,7 +372,7 @@ Rate limits должны быть распределены по двум сло
`nginx`:
- ограничивает частоту запросов до попадания в API;
- держит отдельные зоны лимитов для `/auth`, `/api`, public endpoints, fallback polling и download endpoints;
- держит отдельные зоны лимитов для `/auth`, `/api`, public endpoints, fallback polling, download endpoints, чтения/действий/загрузок Notification Center;
- ограничивает `client_max_body_size`;
- ограничивает загрузку файлов лимитом 5 МБ; `client_max_body_size` должен быть чуть выше бизнес-лимита для учета overhead запроса;
- применяет `limit_req` для endpoint авторизации и fallback polling;
@@ -435,7 +440,8 @@ WAF не заменяет обязательные лимиты, валидац
6. `message-safety`.
7. `bitrix-local-app`.
8. `bitrix-sync`.
9. `nginx`.
9. Notification expire/cleanup workers после готовности `api-backend` и регистрации их entrypoints.
10. `nginx`.
Порядок rollout SMS подробнее задаёт module-11/module-10. Зависимости запуска не образуют цикл: Keycloak стартует при недоступном `sms-service`; это блокирует только новые real-mode orders, а verify уже active challenges продолжается по snapshot.
@@ -92,6 +92,8 @@ Managed PostgreSQL **поднимается до** развёртывания п
| Consent | `consent.personal_data.*`, `consent.privacy_policy.document_url`, `consent.user_agreement.*`, `consent.marketing.*` |
| Файлы чата | `chat.attachments.*` |
| Rate limits (app) | `rate_limit.message_send.*`, `rate_limit.download_url.*`, `rate_limit.public_endpoints.*`, `rate_limit.login.*` |
| Notification Center | `notification.home.max_items`, `notification.center.max_items`, `notification.carousel.*`, `notification.hidden.default_ttl_days`, `notification.documents.max_files`, `notification.expire_job.run_at`, `notification.upload_draft.ttl_days` |
| Rate limits (notifications) | `rate_limit.notifications_read.per_user`, `rate_limit.notifications_action.per_user`, `rate_limit.notification_upload.per_user`, `rate_limit.notifications_public.per_ip` |
| UX | `ux.session.idle_timeout_minutes` |
| Security | `security.cors.allowed_origins`, `security.public_cache.max_age_seconds` |
@@ -135,6 +137,19 @@ rate_limit.message_send.per_dialog=20/minute
rate_limit.download_url.per_user=60/hour
rate_limit.public_endpoints.per_ip=60/minute
rate_limit.login.per_ip=10/minute
rate_limit.notifications_read.per_user=120/minute
rate_limit.notifications_action.per_user=60/minute
rate_limit.notification_upload.per_user=20/minute
rate_limit.notifications_public.per_ip=60/minute
notification.home.max_items=7
notification.center.max_items=15
notification.carousel.autoplay_enabled=false
notification.carousel.autoplay_interval_ms=5000
notification.hidden.default_ttl_days=3
notification.documents.max_files=10
notification.expire_job.run_at=00:01
notification.upload_draft.ttl_days=7
ux.session.idle_timeout_minutes=30
@@ -217,6 +232,10 @@ NGINX_RATE_LIMIT_AUTH=10r/m
NGINX_RATE_LIMIT_DOWNLOADS=30r/m
NGINX_RATE_LIMIT_PUBLIC=60r/m
NGINX_RATE_LIMIT_POLLING=60r/m
NGINX_RATE_LIMIT_NOTIFICATIONS_READ=120r/m
NGINX_RATE_LIMIT_NOTIFICATIONS_ACTION=60r/m
NGINX_RATE_LIMIT_NOTIFICATION_UPLOAD=20r/m
NGINX_RATE_LIMIT_NOTIFICATIONS_PUBLIC=60r/m
# =============================================================================
# Keycloak (mock остаётся true до controlled SMS cutover)
@@ -254,6 +273,7 @@ BITRIX_SYNC_SERVICE_TOKEN=change-me
KEYCLOAK_SETTINGS_BRIDGE_TOKEN=change-me
SMS_SERVICE_TOKEN=change-me
KEYCLOAK_SMS_SERVICE_TOKEN=change-me
NOTIFICATIONS_TOKEN_PRODUCER_TEST=change-me
# =============================================================================
# SMS provider (URL и секреты; runtime-параметры — sms.sms_setting)
@@ -345,6 +365,8 @@ presigned URL и CORS Selectel; path-style адресация не поддер
**Webhook-токены** (публичные callback, не service API): `BITRIX_APPLICATION_TOKEN`, `BITRIX_SYNC_WEBHOOK_TOKEN`.
`NOTIFICATIONS_TOKEN_<SOURCE>` — индивидуальный секрет продюсера Internal Notifications API. Для seed/smoke используется `NOTIFICATIONS_TOKEN_PRODUCER_TEST`; secret хранится только в deployment env/secret, а `notification_sources.token_hash` — только hash. Инструкция не имеет `notification.instruction.allowed_hosts`: она всегда открывается в новой вкладке, iframe-режима нет.
## Namespace переменных Bitrix
- `bitrix-local-app`: `BITRIX_CLIENT_*`, `BITRIX_CONNECTOR_*`, `BITRIX_PUBLIC_BASE_URL`, `BITRIX_DATABASE_URL`, `BITRIX_API_FORWARD_URL`, `BITRIX_APPLICATION_TOKEN` + service tokens.
@@ -20,6 +20,7 @@
- Перечень таблиц, полей, индексов и миграций **определяет модуль-владелец** (`database`, `api-backend`, `bitrix-sync`, `message-safety`, `bitrix-local-app`), а не arch-*.
- Архитектура фиксирует **разделение схем** и общие подходы к ведению баз данных, которые должны соблюдаться при проработке модулей.
- У каждой основной **прикладной** сущности должен быть `record_status`. Базовые статусы: `A` — active, `D` — deleted.
- `record_status` выражает только административное наличие строки. Доменное завершение (например `Notification.lifecycle_status='closed'`) хранится отдельно и не переводит запись в `D`.
- Физическое удаление строк прикладных сущностей запрещено. Если нужно удалить сущность, сервис меняет `record_status` с `A` на `D`.
- При смене статуса на `D` сервис обязан заполнить `status_changed_at` и `status_change_reason`.
- Все сервисы при чтении бизнес-данных по умолчанию запрашивают только `record_status = 'A'`.
@@ -74,6 +75,8 @@ Raw OTP запрещено хранить в открытом виде: это
- поведение при повторной доставке webhook;
- таймауты и retry/backoff.
Для Notification Center обязательны contract tests каталога без ветвления по виду, бессрочной дедупликации `(source, external_id)`, изоляции producer tokens, TTL с сохранением существующего `date_expired`, первого скачивания любого связанного документа и открытия instruction только в новой вкладке.
## Definition of Done
Модуль считается готовым, если:
@@ -85,6 +88,7 @@ Raw OTP запрещено хранить в открытом виде: это
- обновлены seed `app_settings` и `.env.example`, если добавлялись настройки, service tokens, лимиты или feature flags;
- добавлены тесты;
- сервис запускается в Docker Compose;
- worker, указанный в Compose/runbook, имеет реально зарегистрированный entrypoint в image; deployment не может заранее выдумывать имя команды;
- все изменяемые параметры вынесены из кода;
- логи содержат `request_id`, `trace_id` и **`ux_session_id`** (если передан в запросе);
- нет секретов, raw OTP и PII в логах;