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

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

Назначение

Доставка канала Sms (../domain/channel.md) через SMS-агрегатора. Заглушка: реализация отложена — контракт зафиксирован, отправка активируется per-канал флагом (ChannelConfig.IsEnabled). Provider input (delivery-репорты) — недоверенный внешний ввод.

Границы Adapter-а

ПолеЗначение
ProviderSMS-агрегатор (вендор TBD)
Сервисnotifications
Capabilitymessaging (sms)
Adapter classSmsChannelProvider
Typed clientISmsProviderClient
Внутренняя операцияdispatcher при Channel = Sms, Status = Pending
Реализациязаглушка (отложено) — до включения канала отправка не выполняется и возвращается код NTF_PROVIDER_NOT_IMPLEMENTED

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

ОперацияDirectionInternal modelProvider-specific model
sendoutboundNotificationDispatchRequestSmsSendRequest (phone/text/sender)
delivery reportcallbackProviderDeliveryReceiptSmsDeliveryReport (подписанный)

Модель Internal NotificationDispatchRequest

Дом — ../domain/notification.md. Ключевые: NotificationId (idempotency), RecipientAddress.PhoneE164phone, RenderedBodytext (усечение по ChannelConfig.MaxBodyLength; sms без заголовка — RenderedTitle игнорируется).

Модель Provider-Specific SmsSendRequest

ПолеТипОбязательностьProvider formatValidationInternal mapping
phonestringДаE.164валидный номерRecipientAddress.PhoneE164
textstringДа≤ MaxBodyLengthне пустоеRenderedBody (truncate)
senderstringДаalpha-name/номерпо ChannelConfigконфиг
clientReferencestringДаidempotency= NotificationIdNotificationId

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

КонтрольПравило
Authenticationapi-key/подпись запроса агрегатора
Signature validationdelivery-report — HMAC + timestamp + replay-window
Secretsapi-key/секрет вебхука — secret-store, ротация
PIIтелефон/тело — Sensitive; в логах маска (+7***1234), тело не логируется
Tenant bindingполучатель по RecipientUserId; sender — платформенный

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

АспектПравило
Timeout5с к агрегатору
Retry5xx/429/timeout → перевод в Failed → повтор retry-worker-ом (backoff)
Circuit breakerразмыкание на клиенте провайдера
IdempotencyclientReference = NotificationId
Rate limitChannelConfig.RateLimitPerMinute под лимит вендора

Обработка Callback

ШагПравило
Verifyподпись report-а + timestamp
NormalizeSmsDeliveryReport → ProviderDeliveryReceipt (delivered/failed/expired)
ValidateclientReference = известный NotificationId
Dispatchприменить delivery-receipt: перевод уведомления в Sent/Failed
StoreProviderMessageId/статус/LastError на NotificationEntity

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

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

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

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

  1. Выбор к отправке. Dispatcher берёт свежее Pending, retry-worker — Failed с наступившим NextAttemptAt и Attempts < MaxAttempts; уведомление Channel = Sms. Канал выключен (ChannelConfig.IsEnabled = false, заглушка, INV-CHN-2) → попытки нет, статус Pending, исход NTF_PROVIDER_NOT_IMPLEMENTED (skip, метрика channel-disabled).
  2. Резолв адреса. RecipientAddress.PhoneE164phone. Адрес пуст на момент попытки (реплика контактов ещё не пополнена) → уведомление остаётся Pending/Failed до появления адреса (graceful, не терминально); номер невалиден (не E.164) → NTF_PROVIDER_INVALID_RECIPIENT (non-retriable, терминальный Failed).
  3. Сборка сообщения. RenderedBodytext (усечение по ChannelConfig.MaxBodyLength с многоточием, INV-CHN-3); sms без заголовка — RenderedTitle игнорируется; sender — платформенный по ChannelConfig; clientReference = NotificationId (идемпотентность).
  4. Отправка (синхронно, timeout 5с). Повтор той же отправки (clientReference = NotificationId) не создаёт второе sms.
  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;
    • invalid number → NTF_PROVIDER_INVALID_RECIPIENT (non-retriable): терминальный Failed.
  6. Порог повторов. Attempts >= MaxAttempts (INV-NTF-7) → терминальный Failed + NotificationDispatchedEvent(Failed) + алерт; запись выбывает из retry-цикла.

Обратный вызов и согласованность

Delivery-report приходит асинхронно, позже синхронного приёма агрегатором:

  • по clientReference = NotificationId применяется финальный delivery-исход (delivered/failed/expiredSent/Failed); report по неизвестному NotificationId игнорируется (лог).
  • Повтор того же report идемпотентен: терминальный статус не откатывается (INV-NTF-5).
  • Окно согласованности: Sent по приёму агрегатором предшествует финальному report-у (секунды–минуты); при failed-report уведомление может быть переоткрыто в Failed и повторено retry-worker-ом.

Пред- и постусловия

  • Предусловие: канал Sms включён; есть уведомление Channel = Sms в Pending/Failed(due); RecipientAddress.PhoneE164 резолвится (или догоняется апдейтом реплики контактов/retry).
  • Постусловие: уведомление в терминальном Sent (есть ProviderMessageId) либо Failed (терминально после MaxAttempts или invalid number); на финальном исходе эмитирован NotificationDispatchedEvent.

Кросс-сервисные ожидания и гарантии

НаправлениеКонтракт
Ожидает от ChannelConfig (channel.md)IsEnabled, ProviderKey, MaxAttempts, RetryBackoff, RateLimitPerMinute, MaxBodyLength, sender
Ожидает от рендера шаблонанепустой RenderedBody (INV-NTF-2); RenderedTitle игнорируется
Ожидает от адресаRecipientAddress.PhoneE164 (E.164); пусто → ждать апдейта реплики контактов/retry, невалиден → терминальный Failed
Предоставляет агрегатупереход статуса Sent/Failed, ProviderMessageId, LastError (безопасный код)
Предоставляет наружувклад в NotificationDispatchedEvent (ключ партиционирования NotificationId, ContractVersion = 1)
Репликиадрес — снапшот из контактной реплики RecipientContactEntity (ведётся из identity/profile); отставание реплики → адрес пуст, догоняется на retry (не потеря)

Acceptance Criteria

#Given / When / Then
AC1Given канал Sms выключен, When dispatcher берёт Pending, Then попытки нет, статус Pending, метрика channel-disabled (NTF_PROVIDER_NOT_IMPLEMENTED).
AC2Given валидный E.164 и канал включён, When агрегатор принял, Then Sent + ProviderMessageId + вклад в NotificationDispatchedEvent(Sent).
AC3Given 5xx/429, When обработка ответа, Then Failed, Attempts++, NextAttemptAt по backoff; повтор до успеха/MaxAttempts.
AC4Given MaxAttempts исчерпан, When очередная ошибка, Then терминальный Failed + NotificationDispatchedEvent(Failed) + алерт.
AC5Given невалидный номер, When ответ агрегатора, Then терминальный Failed (NTF_PROVIDER_INVALID_RECIPIENT), без повторов.
AC6Given RenderedBody длиннее MaxBodyLength, When сборка sms, Then тело усечено с многоточием (лог truncated, INV-CHN-3).
AC7Given delivery-report по известному NotificationId, When обработка callback, Then статус приведён к report-исходу; повтор report идемпотентен.

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

ТипПравило
Logssend-started/sent/provider-error/report-received/signature-failure (телефон/тело — маска)
Metricsnotifications_sms_sent_total, notifications_sms_error_rate, notifications_sms_retry_total, circuit-state
TracescorrelationId, NotificationId, provider request id
Redactionapi-key/секреты и raw телефон/тело — исключены

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

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

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