Provider Adapter: WhatsApp Messaging (заглушка)
Назначение
Доставка канала WhatsApp (../domain/channel.md) через WhatsApp Business API (Cloud
API/BSP). Заглушка: реализация отложена — контракт зафиксирован, отправка активируется per-канал флагом.
Адрес — телефон E.164; вне 24-часового окна допустимы только утверждённые шаблоны (template message).
Provider input (статусы доставки) — недоверенный внешний ввод.
Границы Adapter-а
| Поле | Значение |
|---|
| Provider | WhatsApp Business API (Cloud API / BSP — вендор TBD) |
| Сервис | notifications |
| Capability | messaging (whatsapp) |
| Adapter class | WhatsAppChannelProvider |
| Typed client | IWhatsAppProviderClient |
| Внутренняя операция | dispatcher при Channel = WhatsApp, Status = Pending |
| Реализация | заглушка (отложено) — до включения канала отправка не выполняется и возвращается код NTF_PROVIDER_NOT_IMPLEMENTED |
Контракты Provider-а
| Операция | Direction | Internal model | Provider-specific model |
|---|
sendMessage | outbound | NotificationDispatchRequest | WhatsAppMessageRequest (to/type/template/body) |
| status webhook | callback | ProviderDeliveryReceipt | WhatsAppStatusWebhook (подписанный) |
Модель Internal NotificationDispatchRequest
Дом — ../domain/notification.md. Ключевые: NotificationId (idempotency),
RecipientAddress.PhoneE164 → to; Kind+Locale → имя утверждённого WhatsApp-шаблона (вне 24ч-окна),
плейсхолдеры PayloadModel → template-параметры; RenderedBody → free-form (только в 24ч-окне).
Модель Provider-Specific WhatsAppMessageRequest
| Поле | Тип | Обязательность | Provider format | Validation | Internal mapping |
|---|
to | string | Да | E.164 (без +) | валидный номер | RecipientAddress.PhoneE164 |
type | string | Да | template/text | вне окна → template | политика окна |
template.name | string | Условно | утверждённый шаблон | зарегистрирован в BSP | Kind+Locale |
template.components | массив | Условно | параметры | ⊆ плейсхолдеров | PayloadModel |
text.body | string | Условно | ≤ 4096 | в 24ч-окне | RenderedBody |
Безопасность
| Контроль | Правило |
|---|
| Authentication | Bearer-token/system-user BSP |
| Signature validation | webhook — X-Hub-Signature-256 (HMAC) + timestamp + replay-window |
| Secrets | access-token/app-secret — secret-store, ротация |
| PII | телефон/тело — Sensitive (маска +7***1234, тело не логируется) |
| Tenant binding | получатель по RecipientUserId; sender-номер — платформенный WABA |
Resilience И Устойчивость
| Аспект | Правило |
|---|
| Timeout | 5с к API |
| Retry | 5xx/429/timeout → перевод в Failed → повтор retry-worker-ом; template not approved/invalid number → не retry |
| Circuit breaker | размыкание на клиенте провайдера |
| Idempotency | client message-ref = NotificationId |
| Rate limit | tier-лимит WABA + ChannelConfig.RateLimitPerMinute |
Обработка Callback
| Шаг | Правило |
|---|
| Verify | HMAC-подпись webhook + timestamp |
| Normalize | WhatsAppStatusWebhook → ProviderDeliveryReceipt (sent/delivered/read/failed) |
| Validate | message-ref = известный NotificationId |
| Dispatch | применить delivery-receipt: перевод уведомления в Sent/Failed (+ ProviderMessageId) |
| Store | статус/ProviderMessageId/LastError на NotificationEntity |
Маппинг Ошибок
| Provider code | Internal error | Повторяемость | Видно пользователю |
|---|
| 5xx / timeout | NTF_PROVIDER_TRANSIENT | Да | Нет |
| 429 / tier limit | NTF_PROVIDER_RATE_LIMITED | Да | Нет |
| template not approved | NTF_WHATSAPP_TEMPLATE_UNAPPROVED | Нет | Нет |
| invalid number / not on WhatsApp | NTF_PROVIDER_INVALID_RECIPIENT | Нет | Нет |
| не активирован (заглушка) | NTF_PROVIDER_NOT_IMPLEMENTED | Нет | Нет |
Поток доставки
Happy path и все ветки отказа отправки уведомления Channel = WhatsApp. Статусы и переходы — по
notification.md (INV-NTF-5/7); коды — таблица «Маппинг Ошибок».
- Выбор к отправке. Dispatcher берёт свежее
Pending, retry-worker — Failed с наступившим
NextAttemptAt и Attempts < MaxAttempts; уведомление Channel = WhatsApp. Канал выключен
(ChannelConfig.IsEnabled = false, заглушка, INV-CHN-2) → попытки нет, статус Pending, исход
NTF_PROVIDER_NOT_IMPLEMENTED (skip, метрика channel-disabled).
- Резолв адреса.
RecipientAddress.PhoneE164 → to (без +). Адрес пуст → уведомление ждёт
апдейта реплики контактов/retry (graceful); номер невалиден / получатель не в WhatsApp → NTF_PROVIDER_INVALID_RECIPIENT
(non-retriable, терминальный Failed).
- Выбор формата по окну сессии. В пределах 24-часового окна — free-form
text.body (≤ 4096) из
RenderedBody; вне окна — только утверждённый шаблон: имя шаблона по (Kind+Locale),
плейсхолдеры PayloadModel → template.components. Шаблон не зарегистрирован/не одобрен в BSP →
NTF_WHATSAPP_TEMPLATE_UNAPPROVED (non-retriable, терминальный Failed).
- Отправка (синхронно, timeout 5с). client message-ref =
NotificationId (идемпотентность): повтор той
же отправки не создаёт второе сообщение.
- Классификация ответа:
- принято BSP →
Pending/Failed → Sent, ProviderMessageId; вклад в
NotificationDispatchedEvent(Sent) (топик pin.notifications.dispatched.v1);
5xx/timeout → NTF_PROVIDER_TRANSIENT (retriable): → Failed, Attempts++,
NextAttemptAt = now + backoff (ChannelConfig.RetryBackoff);
429/tier limit → NTF_PROVIDER_RATE_LIMITED (retriable): дефер по backoff;
- template not approved →
NTF_WHATSAPP_TEMPLATE_UNAPPROVED; invalid number/not on WhatsApp →
NTF_PROVIDER_INVALID_RECIPIENT — оба non-retriable, терминальный Failed.
- Порог повторов.
Attempts >= MaxAttempts (INV-NTF-7) → терминальный Failed +
NotificationDispatchedEvent(Failed) + алерт; запись выбывает из retry-цикла.
Обратный вызов и согласованность
Status webhook приходит асинхронно, позже синхронного приёма BSP:
- по message-ref =
NotificationId применяется delivery-исход (sent/delivered/read/failed →
Sent/Failed, + ProviderMessageId); webhook по неизвестному NotificationId игнорируется (лог).
- Повтор того же webhook идемпотентен: терминальный статус не откатывается (INV-NTF-5);
read после
delivered не меняет Sent.
- Окно согласованности:
Sent по приёму BSP предшествует финальному webhook-у; failed-webhook может
переоткрыть уведомление в Failed и вернуть в retry-цикл.
Пред- и постусловия
- Предусловие: канал
WhatsApp включён; есть уведомление Channel = WhatsApp в Pending/Failed(due);
RecipientAddress.PhoneE164 резолвится; вне 24ч-окна — существует утверждённый шаблон (Kind, Locale).
- Постусловие: уведомление в терминальном
Sent (есть ProviderMessageId) либо Failed (терминально
после MaxAttempts / unapproved-template / invalid number); на финальном исходе эмитирован
NotificationDispatchedEvent.
Кросс-сервисные ожидания и гарантии
| Направление | Контракт |
|---|
Ожидает от ChannelConfig (channel.md) | IsEnabled, ProviderKey, MaxAttempts, RetryBackoff, RateLimitPerMinute |
| Ожидает от рендера/шаблона | непустой RenderedBody (в 24ч-окне); вне окна — утверждённый шаблон (Kind, Locale) и PayloadModel для плейсхолдеров (notification-template.md) |
| Ожидает от адреса | 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 канал WhatsApp выключен, When dispatcher берёт Pending, Then попытки нет, статус Pending, метрика channel-disabled (NTF_PROVIDER_NOT_IMPLEMENTED). |
| AC2 | Given валидный E.164 и канал включён, When BSP принял, Then Sent + ProviderMessageId + вклад в NotificationDispatchedEvent(Sent). |
| AC3 | Given вне 24ч-окна и шаблон одобрен, When отправка, Then уходит template message; при отсутствии одобрения — терминальный Failed (NTF_WHATSAPP_TEMPLATE_UNAPPROVED). |
| AC4 | Given 5xx/429, When обработка ответа, Then Failed, Attempts++, NextAttemptAt по backoff; повтор до успеха/MaxAttempts; после MaxAttempts — терминальный Failed + NotificationDispatchedEvent(Failed) + алерт. |
| AC5 | Given невалидный номер / не в WhatsApp, When ответ BSP, Then терминальный Failed (NTF_PROVIDER_INVALID_RECIPIENT), без повторов. |
| AC6 | Given в пределах 24ч-окна, When отправка, Then допускается free-form text.body (≤ 4096) из RenderedBody. |
| AC7 | Given status webhook по известному NotificationId, When обработка callback, Then статус приведён к webhook-исходу; повтор идемпотентен. |
Наблюдаемость
| Тип | Правило |
|---|
| Logs | send-started/sent/template-unapproved/provider-error/webhook-received/signature-failure (телефон/тело — маска) |
| Metrics | notifications_whatsapp_sent_total, notifications_whatsapp_error_rate, notifications_whatsapp_retry_total, circuit-state |
| Traces | correlationId, NotificationId, provider message id |
| Redaction | access-token/secret и raw телефон/тело — исключены |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы