Перейти к основному содержимому

Provider Adapter: Push Messaging (заглушка)

Назначение

Доставка канала Push (../domain/channel.md) через push-шлюз (FCM/APNs/web-push). Заглушка: реализация отложена — контракт зафиксирован, отправка активируется per-канал флагом. Реестр device-токенов ведёт адаптер (по RecipientUserId) — уведомление хранит только RecipientUserId, не токены. Provider input (delivery-receipt) — недоверенный внешний ввод.

Границы Adapter-а

ПолеЗначение
ProviderPush-шлюз (FCM/APNs/web-push — вендор TBD)
Сервисnotifications
Capabilitymessaging (push)
Adapter classPushChannelProvider
Typed clientIPushProviderClient + IDeviceTokenStore (реестр токенов по UserId)
Внутренняя операцияdispatcher при Channel = Push, Status = Pending
Реализациязаглушка (отложено) — до включения канала отправка не выполняется и возвращается код NTF_PROVIDER_NOT_IMPLEMENTED

Контракты Provider-а

ОперацияDirectionInternal modelProvider-specific model
sendoutboundNotificationDispatchRequestPushSendRequest (tokens/title/body/data)
delivery/token-invalidationcallbackProviderDeliveryReceiptPushDeliveryReceipt
register device tokeninboundрегистрация device-токена (mobile API)DeviceToken

Модель Internal NotificationDispatchRequest

Дом — ../domain/notification.md. Ключевые: NotificationId (idempotency), RecipientUserId → резолв device-токенов в IDeviceTokenStore, RenderedTitletitle, RenderedBodybody, DeepLinkdata.deepLink.

Модель Provider-Specific PushSendRequest

ПолеТипОбязательностьProvider formatValidationInternal mapping
tokensIReadOnlyList<string>Даdevice tokensне пусто (нет токенов → skip)IDeviceTokenStore.Get(RecipientUserId)
titlestringДа≤ 256RenderedTitle
bodystringДаtextне пустоеRenderedBody
data.deepLinkstring?НетuriDeepLink
collapseKeystringДаidempotency= NotificationIdNotificationId

Безопасность

КонтрольПравило
Authenticationсервис-аккаунт FCM/APNs-ключ
Signature validationdelivery-callback — подпись шлюза + timestamp
Secretsключи FCM/APNs — secret-store, ротация
PIIтело/заголовок — Sensitive (маска в логах); device-токены — Sensitive, не логируются
Tenant bindingтокены привязаны к RecipientUserId; кросс-пользовательская доставка исключена

Resilience И Устойчивость

АспектПравило
Timeout5с к шлюзу
Retry5xx/429/timeout → перевод в Failed → повтор retry-worker-ом; Unregistered-токен → удалить из store, не retry
Circuit breakerразмыкание на клиенте провайдера
IdempotencycollapseKey = NotificationId
Rate limitпо лимиту шлюза (ChannelConfig)

Обработка Callback

ШагПравило
Verifyподпись callback + timestamp
NormalizePushDeliveryReceipt → ProviderDeliveryReceipt; token-invalidation → удаление токена
ValidatecollapseKey = известный NotificationId
Dispatchприменить delivery-receipt: перевод уведомления в Sent/Failed
Storeстатус/ProviderMessageId на NotificationEntity; реестр токенов — в IDeviceTokenStore

Маппинг Ошибок

Provider codeInternal errorПовторяемостьВидно пользователю
5xx / timeoutNTF_PROVIDER_TRANSIENTДаНет
429NTF_PROVIDER_RATE_LIMITEDДаНет
unregistered tokenNTF_PUSH_TOKEN_INVALIDНет (удалить токен)Нет
нет токенов у пользователяNTF_PUSH_NO_DEVICEНет (skip)Нет
не активирован (заглушка)NTF_PROVIDER_NOT_IMPLEMENTEDНетНет

Поток доставки

Happy path и все ветки отказа отправки уведомления Channel = Push. Статусы и переходы — по notification.md (INV-NTF-5/7); коды — таблица «Маппинг Ошибок».

  1. Выбор к отправке. Dispatcher берёт свежее Pending, retry-worker — Failed с наступившим NextAttemptAt и Attempts < MaxAttempts; уведомление Channel = Push. Канал выключен (ChannelConfig.IsEnabled = false, заглушка, INV-CHN-2) → попытки нет, статус Pending, исход NTF_PROVIDER_NOT_IMPLEMENTED (skip, метрика channel-disabled).
  2. Резолв токенов. зарегистрированные device-токены по RecipientUserId (реестр токенов ведёт адаптер). Токенов нет → NTF_PUSH_NO_DEVICE: канал пропускается (non-retriable); получатель покрыт другими каналами (in-app — всегда).
  3. Сборка уведомления. RenderedTitletitle (≤ 256), RenderedBodybody, DeepLinkdata.deepLink, collapseKey = NotificationId (идемпотентность/схлопывание у шлюза). Уведомление уходит на все активные токены пользователя (мультидевайс — один NotificationEntity, несколько адресатов шлюза).
  4. Отправка (синхронно, timeout 5с). Повтор той же отправки (collapseKey = NotificationId) не создаёт второй push на устройстве.
  5. Классификация ответа:
    • успех (доставлено шлюзу) → Pending/Failed → Sent, ProviderMessageId; вклад в NotificationDispatchedEvent(Sent) (топик pin.notifications.dispatched.v1);
    • 5xx/timeout → NTF_PROVIDER_TRANSIENT (retriable): → Failed, Attempts++, NextAttemptAt = now + backoff (ChannelConfig.RetryBackoff);
    • 429NTF_PROVIDER_RATE_LIMITED (retriable): дефер по backoff;
    • Unregistered-токен → NTF_PUSH_TOKEN_INVALID (non-retriable для этого токена): токен удаляется из store; если валидных токенов не осталось — терминальный Failed.
  6. Порог повторов. 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
AC1Given канал Push выключен, When dispatcher берёт Pending, Then попытки нет, статус Pending, метрика channel-disabled (NTF_PROVIDER_NOT_IMPLEMENTED).
AC2Given есть валидные токены и канал включён, When отправка успешна, Then Sent + ProviderMessageId + вклад в NotificationDispatchedEvent(Sent).
AC3Given 5xx/429, When обработка ответа, Then Failed, Attempts++, NextAttemptAt по backoff; повтор до успеха/MaxAttempts.
AC4Given MaxAttempts исчерпан, When очередная ошибка, Then терминальный Failed + NotificationDispatchedEvent(Failed) + алерт.
AC5Given Unregistered-токен, When ответ шлюза, Then токен удалён из store (NTF_PUSH_TOKEN_INVALID); при отсутствии валидных токенов — терминальный Failed.
AC6Given у пользователя нет токенов, When отправка, Then канал пропущен (NTF_PUSH_NO_DEVICE), in-app доставлен независимо.
AC7Given delivery-receipt по известному NotificationId, When обработка callback, Then статус приведён к receipt-исходу; повтор receipt идемпотентен.

Наблюдаемость

ТипПравило
Logssend-started/sent/token-invalidated/no-device/provider-error (тело/токены — маска)
Metricsnotifications_push_sent_total, notifications_push_error_rate, notifications_push_token_invalidated_total, circuit-state
TracescorrelationId, NotificationId, provider request id
Redactionключи шлюза и device-токены/тело — исключены

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы