Provider Adapter: SMS Messaging (заглушка)
Назначение
Доставка канала Sms (../domain/channel.md) через SMS-агрегатора. Заглушка:
реализация отложена — контракт зафиксирован, отправка активируется per-канал флагом
(ChannelConfig.IsEnabled). Provider input (delivery-репорты) — недоверенный внешний ввод.
Границы Adapter-а
| Поле | Значение |
|---|
| Provider | SMS-агрегатор (вендор TBD) |
| Сервис | notifications |
| Capability | messaging (sms) |
| Adapter class | SmsChannelProvider |
| Typed client | ISmsProviderClient |
| Внутренняя операция | dispatcher при Channel = Sms, Status = Pending |
| Реализация | заглушка (отложено) — до включения канала отправка не выполняется и возвращается код NTF_PROVIDER_NOT_IMPLEMENTED |
Контракты Provider-а
| Операция | Direction | Internal model | Provider-specific model |
|---|
send | outbound | NotificationDispatchRequest | SmsSendRequest (phone/text/sender) |
| delivery report | callback | ProviderDeliveryReceipt | SmsDeliveryReport (подписанный) |
Модель Internal NotificationDispatchRequest
Дом — ../domain/notification.md. Ключевые: NotificationId (idempotency),
RecipientAddress.PhoneE164 → phone, RenderedBody → text (усечение по ChannelConfig.MaxBodyLength;
sms без заголовка — RenderedTitle игнорируется).
Модель Provider-Specific SmsSendRequest
| Поле | Тип | Обязательность | Provider format | Validation | Internal mapping |
|---|
phone | string | Да | E.164 | валидный номер | RecipientAddress.PhoneE164 |
text | string | Да | ≤ MaxBodyLength | не пустое | RenderedBody (truncate) |
sender | string | Да | alpha-name/номер | по ChannelConfig | конфиг |
clientReference | string | Да | idempotency | = NotificationId | NotificationId |
Безопасность
| Контроль | Правило |
|---|
| Authentication | api-key/подпись запроса агрегатора |
| Signature validation | delivery-report — HMAC + timestamp + replay-window |
| Secrets | api-key/секрет вебхука — secret-store, ротация |
| PII | телефон/тело — Sensitive; в логах маска (+7***1234), тело не логируется |
| Tenant binding | получатель по RecipientUserId; sender — платформенный |
Resilience И Устойчивость
| Аспект | Правило |
|---|
| Timeout | 5с к агрегатору |
| Retry | 5xx/429/timeout → перевод в Failed → повтор retry-worker-ом (backoff) |
| Circuit breaker | размыкание на клиенте провайдера |
| Idempotency | clientReference = NotificationId |
| Rate limit | ChannelConfig.RateLimitPerMinute под лимит вендора |
Обработка Callback
| Шаг | Правило |
|---|
| Verify | подпись report-а + timestamp |
| Normalize | SmsDeliveryReport → ProviderDeliveryReceipt (delivered/failed/expired) |
| Validate | clientReference = известный NotificationId |
| Dispatch | применить delivery-receipt: перевод уведомления в Sent/Failed |
| Store | ProviderMessageId/статус/LastError на NotificationEntity |
Маппинг Ошибок
| Provider code | Internal error | Повторяемость | Видно пользователю |
|---|
| 5xx / timeout | NTF_PROVIDER_TRANSIENT | Да | Нет |
| 429 | NTF_PROVIDER_RATE_LIMITED | Да | Нет |
| invalid number | NTF_PROVIDER_INVALID_RECIPIENT | Нет | Нет |
| не активирован (заглушка) | NTF_PROVIDER_NOT_IMPLEMENTED | Нет | Нет |
Поток доставки
Happy path и все ветки отказа отправки уведомления Channel = Sms. Статусы и переходы — по
notification.md (INV-NTF-5/7); коды — таблица «Маппинг Ошибок».
- Выбор к отправке. Dispatcher берёт свежее
Pending, retry-worker — Failed с наступившим
NextAttemptAt и Attempts < MaxAttempts; уведомление Channel = Sms. Канал выключен
(ChannelConfig.IsEnabled = false, заглушка, INV-CHN-2) → попытки нет, статус Pending, исход
NTF_PROVIDER_NOT_IMPLEMENTED (skip, метрика channel-disabled).
- Резолв адреса.
RecipientAddress.PhoneE164 → phone. Адрес пуст на момент попытки (реплика контактов
ещё не пополнена) → уведомление остаётся Pending/Failed до появления адреса (graceful, не терминально);
номер невалиден (не E.164) → NTF_PROVIDER_INVALID_RECIPIENT (non-retriable, терминальный Failed).
- Сборка сообщения.
RenderedBody → text (усечение по ChannelConfig.MaxBodyLength с многоточием,
INV-CHN-3); sms без заголовка — RenderedTitle игнорируется; sender — платформенный по ChannelConfig;
clientReference = NotificationId (идемпотентность).
- Отправка (синхронно, timeout 5с). Повтор той же отправки (
clientReference = NotificationId) не
создаёт второе sms.
- Классификация ответа:
- принято агрегатором →
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;
- invalid number →
NTF_PROVIDER_INVALID_RECIPIENT (non-retriable): терминальный Failed.
- Порог повторов.
Attempts >= MaxAttempts (INV-NTF-7) → терминальный Failed +
NotificationDispatchedEvent(Failed) + алерт; запись выбывает из retry-цикла.
Обратный вызов и согласованность
Delivery-report приходит асинхронно, позже синхронного приёма агрегатором:
- по
clientReference = NotificationId применяется финальный delivery-исход (delivered/failed/expired →
Sent/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 |
|---|
| AC1 | Given канал Sms выключен, When dispatcher берёт Pending, Then попытки нет, статус Pending, метрика channel-disabled (NTF_PROVIDER_NOT_IMPLEMENTED). |
| AC2 | Given валидный E.164 и канал включён, 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 невалидный номер, When ответ агрегатора, Then терминальный Failed (NTF_PROVIDER_INVALID_RECIPIENT), без повторов. |
| AC6 | Given RenderedBody длиннее MaxBodyLength, When сборка sms, Then тело усечено с многоточием (лог truncated, INV-CHN-3). |
| AC7 | Given delivery-report по известному NotificationId, When обработка callback, Then статус приведён к report-исходу; повтор report идемпотентен. |
Наблюдаемость
| Тип | Правило |
|---|
| Logs | send-started/sent/provider-error/report-received/signature-failure (телефон/тело — маска) |
| Metrics | notifications_sms_sent_total, notifications_sms_error_rate, notifications_sms_retry_total, circuit-state |
| Traces | correlationId, NotificationId, provider request id |
| Redaction | api-key/секреты и raw телефон/тело — исключены |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы