Provider Adapter: Push Messaging (заглушка)
Назначение
Доставка канала Push (../domain/channel.md) через push-шлюз (FCM/APNs/web-push).
Заглушка: реализация отложена — контракт зафиксирован, отправка активируется per-канал флагом. Реестр
device-токенов ведёт адаптер (по RecipientUserId) — уведомление хранит только RecipientUserId, не токены.
Provider input (delivery-receipt) — недоверенный внешний ввод.
Границы Adapter-а
| Поле | Значение |
|---|
| Provider | Push-шлюз (FCM/APNs/web-push — вендор TBD) |
| Сервис | notifications |
| Capability | messaging (push) |
| Adapter class | PushChannelProvider |
| Typed client | IPushProviderClient + IDeviceTokenStore (реестр токенов по UserId) |
| Внутренняя операция | dispatcher при Channel = Push, Status = Pending |
| Реализация | заглушка (отложено) — до включения канала отправка не выполняется и возвращается код NTF_PROVIDER_NOT_IMPLEMENTED |
Контракты Provider-а
| Операция | Direction | Internal model | Provider-specific model |
|---|
send | outbound | NotificationDispatchRequest | PushSendRequest (tokens/title/body/data) |
| delivery/token-invalidation | callback | ProviderDeliveryReceipt | PushDeliveryReceipt |
| register device token | inbound | регистрация device-токена (mobile API) | DeviceToken |
Модель Internal NotificationDispatchRequest
Дом — ../domain/notification.md. Ключевые: NotificationId (idempotency),
RecipientUserId → резолв device-токенов в IDeviceTokenStore, RenderedTitle → title, RenderedBody
→ body, DeepLink → data.deepLink.
Модель Provider-Specific PushSendRequest
| Поле | Тип | Обязательность | Provider format | Validation | Internal mapping |
|---|
tokens | IReadOnlyList<string> | Да | device tokens | не пусто (нет токенов → skip) | IDeviceTokenStore.Get(RecipientUserId) |
title | string | Да | ≤ 256 | — | RenderedTitle |
body | string | Да | text | не пустое | RenderedBody |
data.deepLink | string? | Нет | uri | — | DeepLink |
collapseKey | string | Да | idempotency | = NotificationId | NotificationId |
Безопасность
| Контроль | Правило |
|---|
| Authentication | сервис-аккаунт FCM/APNs-ключ |
| Signature validation | delivery-callback — подпись шлюза + timestamp |
| Secrets | ключи FCM/APNs — secret-store, ротация |
| PII | тело/заголовок — Sensitive (маска в логах); device-токены — Sensitive, не логируются |
| Tenant binding | токены привязаны к RecipientUserId; кросс-пользовательская доставка исключена |
Resilience И Устойчивость
| Аспект | Правило |
|---|
| Timeout | 5с к шлюзу |
| Retry | 5xx/429/timeout → перевод в Failed → повтор retry-worker-ом; Unregistered-токен → удалить из store, не retry |
| Circuit breaker | размыкание на клиенте провайдера |
| Idempotency | collapseKey = NotificationId |
| Rate limit | по лимиту шлюза (ChannelConfig) |
Обработка Callback
| Шаг | Правило |
|---|
| Verify | подпись callback + timestamp |
| Normalize | PushDeliveryReceipt → ProviderDeliveryReceipt; token-invalidation → удаление токена |
| Validate | collapseKey = известный NotificationId |
| Dispatch | применить delivery-receipt: перевод уведомления в Sent/Failed |
| Store | статус/ProviderMessageId на NotificationEntity; реестр токенов — в IDeviceTokenStore |
Маппинг Ошибок
| Provider code | Internal error | Повторяемость | Видно пользователю |
|---|
| 5xx / timeout | NTF_PROVIDER_TRANSIENT | Да | Нет |
| 429 | NTF_PROVIDER_RATE_LIMITED | Да | Нет |
| unregistered token | NTF_PUSH_TOKEN_INVALID | Нет (удалить токен) | Нет |
| нет токенов у пользователя | NTF_PUSH_NO_DEVICE | Нет (skip) | Нет |
| не активирован (заглушка) | NTF_PROVIDER_NOT_IMPLEMENTED | Нет | Нет |
Поток доставки
Happy path и все ветки отказа отправки уведомления Channel = Push. Статусы и переходы — по
notification.md (INV-NTF-5/7); коды — таблица «Маппинг Ошибок».
- Выбор к отправке. Dispatcher берёт свежее
Pending, retry-worker — Failed с наступившим
NextAttemptAt и Attempts < MaxAttempts; уведомление Channel = Push. Канал выключен
(ChannelConfig.IsEnabled = false, заглушка, INV-CHN-2) → попытки нет, статус Pending, исход
NTF_PROVIDER_NOT_IMPLEMENTED (skip, метрика channel-disabled).
- Резолв токенов. зарегистрированные device-токены по
RecipientUserId (реестр токенов ведёт адаптер).
Токенов нет → NTF_PUSH_NO_DEVICE: канал пропускается (non-retriable); получатель покрыт другими каналами
(in-app — всегда).
- Сборка уведомления.
RenderedTitle → title (≤ 256), RenderedBody → body, DeepLink →
data.deepLink, collapseKey = NotificationId (идемпотентность/схлопывание у шлюза). Уведомление уходит
на все активные токены пользователя (мультидевайс — один NotificationEntity, несколько адресатов
шлюза).
- Отправка (синхронно, timeout 5с). Повтор той же отправки (
collapseKey = NotificationId) не создаёт
второй push на устройстве.
- Классификация ответа:
- успех (доставлено шлюзу) →
Pending/Failed → Sent, ProviderMessageId; вклад в
NotificationDispatchedEvent(Sent) (топик pin.notifications.dispatched.v1);
5xx/timeout → NTF_PROVIDER_TRANSIENT (retriable): → Failed, Attempts++,
NextAttemptAt = now + backoff (ChannelConfig.RetryBackoff);
429 → NTF_PROVIDER_RATE_LIMITED (retriable): дефер по backoff;
Unregistered-токен → NTF_PUSH_TOKEN_INVALID (non-retriable для этого токена): токен удаляется из
store; если валидных токенов не осталось — терминальный Failed.
- Порог повторов.
Attempts >= MaxAttempts (INV-NTF-7) → терминальный Failed +
NotificationDispatchedEvent(Failed) + алерт; запись выбывает из retry-цикла.
Обратный вызов и согласованность
Delivery-receipt / token-invalidation приходят асинхронно, позже синхронного ответа шлюза:
- receipt по
collapseKey = NotificationId: применяется delivery-исход (перевод в Sent/Failed); receipt
по неизвестному NotificationId игнорируется (лог).
- token-invalidation → удаление токена из реестра; на статус уже отправленного уведомления не
влияет ретроспективно.
- Повтор того же receipt идемпотентен (терминальный статус не откатывается — INV-NTF-5).
- Окно согласованности: между
Sent по синхронному ответу и приходом receipt — секунды; итоговый статус
определяется receipt-ом.
Пред- и постусловия
- Предусловие: канал
Push включён; есть уведомление Channel = Push в Pending/Failed(due); для
доставки — зарегистрирован ≥1 device-токен (mobile API).
- Постусловие: уведомление в терминальном
Sent (есть ProviderMessageId) либо Failed (терминально
после MaxAttempts / отсутствия валидных токенов); на финальном исходе эмитирован
NotificationDispatchedEvent; невалидные токены вычищены из store.
Кросс-сервисные ожидания и гарантии
| Направление | Контракт |
|---|
Ожидает от ChannelConfig (channel.md) | IsEnabled, ProviderKey, MaxAttempts, RetryBackoff, RateLimitPerMinute |
| Ожидает от рендера шаблона | непустой RenderedBody (INV-NTF-2); опц. RenderedTitle/DeepLink |
| Ожидает от реестра токенов | ≥1 активный device-токен по RecipientUserId; пусто → skip |
| Предоставляет агрегату | переход статуса Sent/Failed, ProviderMessageId, LastError (безопасный код) |
| Предоставляет наружу | вклад в NotificationDispatchedEvent (ключ партиционирования NotificationId, ContractVersion = 1) |
| Реплики | реестр токенов ведёт адаптер; недоступность шлюза — transient-retry, не потеря |
Acceptance Criteria
| # | Given / When / Then |
|---|
| AC1 | Given канал Push выключен, When dispatcher берёт Pending, Then попытки нет, статус Pending, метрика channel-disabled (NTF_PROVIDER_NOT_IMPLEMENTED). |
| AC2 | Given есть валидные токены и канал включён, When отправка успешна, Then Sent + ProviderMessageId + вклад в NotificationDispatchedEvent(Sent). |
| AC3 | Given 5xx/429, When обработка ответа, Then Failed, Attempts++, NextAttemptAt по backoff; повтор до успеха/MaxAttempts. |
| AC4 | Given MaxAttempts исчерпан, When очередная ошибка, Then терминальный Failed + NotificationDispatchedEvent(Failed) + алерт. |
| AC5 | Given Unregistered-токен, When ответ шлюза, Then токен удалён из store (NTF_PUSH_TOKEN_INVALID); при отсутствии валидных токенов — терминальный Failed. |
| AC6 | Given у пользователя нет токенов, When отправка, Then канал пропущен (NTF_PUSH_NO_DEVICE), in-app доставлен независимо. |
| AC7 | Given delivery-receipt по известному NotificationId, When обработка callback, Then статус приведён к receipt-исходу; повтор receipt идемпотентен. |
Наблюдаемость
| Тип | Правило |
|---|
| Logs | send-started/sent/token-invalidated/no-device/provider-error (тело/токены — маска) |
| Metrics | notifications_push_sent_total, notifications_push_error_rate, notifications_push_token_invalidated_total, circuit-state |
| Traces | correlationId, NotificationId, provider request id |
| Redaction | ключи шлюза и device-токены/тело — исключены |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы