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

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

Назначение

Доставка канала WhatsApp (../domain/channel.md) через WhatsApp Business API (Cloud API/BSP). Заглушка: реализация отложена — контракт зафиксирован, отправка активируется per-канал флагом. Адрес — телефон E.164; вне 24-часового окна допустимы только утверждённые шаблоны (template message). Provider input (статусы доставки) — недоверенный внешний ввод.

Границы Adapter-а

ПолеЗначение
ProviderWhatsApp Business API (Cloud API / BSP — вендор TBD)
Сервисnotifications
Capabilitymessaging (whatsapp)
Adapter classWhatsAppChannelProvider
Typed clientIWhatsAppProviderClient
Внутренняя операцияdispatcher при Channel = WhatsApp, Status = Pending
Реализациязаглушка (отложено) — до включения канала отправка не выполняется и возвращается код NTF_PROVIDER_NOT_IMPLEMENTED

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

ОперацияDirectionInternal modelProvider-specific model
sendMessageoutboundNotificationDispatchRequestWhatsAppMessageRequest (to/type/template/body)
status webhookcallbackProviderDeliveryReceiptWhatsAppStatusWebhook (подписанный)

Модель Internal NotificationDispatchRequest

Дом — ../domain/notification.md. Ключевые: NotificationId (idempotency), RecipientAddress.PhoneE164to; Kind+Localeимя утверждённого WhatsApp-шаблона (вне 24ч-окна), плейсхолдеры PayloadModel → template-параметры; RenderedBody → free-form (только в 24ч-окне).

Модель Provider-Specific WhatsAppMessageRequest

ПолеТипОбязательностьProvider formatValidationInternal mapping
tostringДаE.164 (без +)валидный номерRecipientAddress.PhoneE164
typestringДаtemplate/textвне окна → templateполитика окна
template.namestringУсловноутверждённый шаблонзарегистрирован в BSPKind+Locale
template.componentsмассивУсловнопараметры⊆ плейсхолдеровPayloadModel
text.bodystringУсловно≤ 4096в 24ч-окнеRenderedBody

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

КонтрольПравило
AuthenticationBearer-token/system-user BSP
Signature validationwebhook — X-Hub-Signature-256 (HMAC) + timestamp + replay-window
Secretsaccess-token/app-secret — secret-store, ротация
PIIтелефон/тело — Sensitive (маска +7***1234, тело не логируется)
Tenant bindingполучатель по RecipientUserId; sender-номер — платформенный WABA

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

АспектПравило
Timeout5с к API
Retry5xx/429/timeout → перевод в Failed → повтор retry-worker-ом; template not approved/invalid number → не retry
Circuit breakerразмыкание на клиенте провайдера
Idempotencyclient message-ref = NotificationId
Rate limittier-лимит WABA + ChannelConfig.RateLimitPerMinute

Обработка Callback

ШагПравило
VerifyHMAC-подпись webhook + timestamp
NormalizeWhatsAppStatusWebhook → ProviderDeliveryReceipt (sent/delivered/read/failed)
Validatemessage-ref = известный NotificationId
Dispatchприменить delivery-receipt: перевод уведомления в Sent/Failed (+ ProviderMessageId)
Storeстатус/ProviderMessageId/LastError на NotificationEntity

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

Provider codeInternal errorПовторяемостьВидно пользователю
5xx / timeoutNTF_PROVIDER_TRANSIENTДаНет
429 / tier limitNTF_PROVIDER_RATE_LIMITEDДаНет
template not approvedNTF_WHATSAPP_TEMPLATE_UNAPPROVEDНетНет
invalid number / not on WhatsAppNTF_PROVIDER_INVALID_RECIPIENTНетНет
не активирован (заглушка)NTF_PROVIDER_NOT_IMPLEMENTEDНетНет

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

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

  1. Выбор к отправке. Dispatcher берёт свежее Pending, retry-worker — Failed с наступившим NextAttemptAt и Attempts < MaxAttempts; уведомление Channel = WhatsApp. Канал выключен (ChannelConfig.IsEnabled = false, заглушка, INV-CHN-2) → попытки нет, статус Pending, исход NTF_PROVIDER_NOT_IMPLEMENTED (skip, метрика channel-disabled).
  2. Резолв адреса. RecipientAddress.PhoneE164to (без +). Адрес пуст → уведомление ждёт апдейта реплики контактов/retry (graceful); номер невалиден / получатель не в WhatsApp → NTF_PROVIDER_INVALID_RECIPIENT (non-retriable, терминальный Failed).
  3. Выбор формата по окну сессии. В пределах 24-часового окна — free-form text.body (≤ 4096) из RenderedBody; вне окна — только утверждённый шаблон: имя шаблона по (Kind+Locale), плейсхолдеры PayloadModeltemplate.components. Шаблон не зарегистрирован/не одобрен в BSP → NTF_WHATSAPP_TEMPLATE_UNAPPROVED (non-retriable, терминальный Failed).
  4. Отправка (синхронно, timeout 5с). client message-ref = NotificationId (идемпотентность): повтор той же отправки не создаёт второе сообщение.
  5. Классификация ответа:
    • принято 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.
  6. Порог повторов. Attempts >= MaxAttempts (INV-NTF-7) → терминальный Failed + NotificationDispatchedEvent(Failed) + алерт; запись выбывает из retry-цикла.

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

Status webhook приходит асинхронно, позже синхронного приёма BSP:

  • по message-ref = NotificationId применяется delivery-исход (sent/delivered/read/failedSent/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
AC1Given канал WhatsApp выключен, When dispatcher берёт Pending, Then попытки нет, статус Pending, метрика channel-disabled (NTF_PROVIDER_NOT_IMPLEMENTED).
AC2Given валидный E.164 и канал включён, When BSP принял, Then Sent + ProviderMessageId + вклад в NotificationDispatchedEvent(Sent).
AC3Given вне 24ч-окна и шаблон одобрен, When отправка, Then уходит template message; при отсутствии одобрения — терминальный Failed (NTF_WHATSAPP_TEMPLATE_UNAPPROVED).
AC4Given 5xx/429, When обработка ответа, Then Failed, Attempts++, NextAttemptAt по backoff; повтор до успеха/MaxAttempts; после MaxAttempts — терминальный Failed + NotificationDispatchedEvent(Failed) + алерт.
AC5Given невалидный номер / не в WhatsApp, When ответ BSP, Then терминальный Failed (NTF_PROVIDER_INVALID_RECIPIENT), без повторов.
AC6Given в пределах 24ч-окна, When отправка, Then допускается free-form text.body (≤ 4096) из RenderedBody.
AC7Given status webhook по известному NotificationId, When обработка callback, Then статус приведён к webhook-исходу; повтор идемпотентен.

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

ТипПравило
Logssend-started/sent/template-unapproved/provider-error/webhook-received/signature-failure (телефон/тело — маска)
Metricsnotifications_whatsapp_sent_total, notifications_whatsapp_error_rate, notifications_whatsapp_retry_total, circuit-state
TracescorrelationId, NotificationId, provider message id
Redactionaccess-token/secret и raw телефон/тело — исключены

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

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

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