Provider Adapter: Viber Messaging (заглушка)
Назначение
Доставка канала Viber (../domain/channel.md) через Viber Business Messages
(транзакционные сообщения). Заглушка: реализация отложена — контракт зафиксирован, отправка активируется
per-канал флагом. Адрес — телефон E.164 (Business Messages) или Viber user-id (public-account подписчик).
Provider input (delivery receipts) — недоверенный внешний ввод.
Границы Adapter-а
| Поле | Значение |
|---|
| Provider | Viber Business Messages (BSP — вендор TBD) |
| Сервис | notifications |
| Capability | messaging (viber) |
| Adapter class | ViberChannelProvider |
| Typed client | IViberProviderClient |
| Внутренняя операция | dispatcher при Channel = Viber, Status = Pending |
| Реализация | заглушка (отложено) — до включения канала отправка не выполняется и возвращается код NTF_PROVIDER_NOT_IMPLEMENTED |
Контракты Provider-а
| Операция | Direction | Internal model | Provider-specific model |
|---|
sendMessage | outbound | NotificationDispatchRequest | ViberMessageRequest (receiver/type/text/keyboard) |
| delivery/seen webhook | callback | ProviderDeliveryReceipt | ViberStatusWebhook (подписанный) |
Модель Internal NotificationDispatchRequest
Дом — ../domain/notification.md. Ключевые: NotificationId (idempotency),
RecipientAddress.PhoneE164 (Business) / MessengerHandle (public-account) → receiver,
RenderedTitle+RenderedBody → text, DeepLink → keyboard-кнопка. При недоставке в Viber возможен
sms-fallback вендора (учитывается как отдельный billing-исход, здесь — статус Sent).
Модель Provider-Specific ViberMessageRequest
| Поле | Тип | Обязательность | Provider format | Validation | Internal mapping |
|---|
receiver | string | Да | E.164 / Viber id | валидный адрес (иначе skip) | RecipientAddress |
type | string | Да | text | — | конфиг |
text | string | Да | ≤ 1000 | не пустое | RenderedTitle+RenderedBody |
keyboard | объект? | Нет | Viber keyboard | — | DeepLink |
clientReference | string | Да | idempotency | = NotificationId | NotificationId |
Безопасность
| Контроль | Правило |
|---|
| Authentication | api-key/auth-token BSP |
| Signature validation | webhook — HMAC/подпись BSP + timestamp + replay-window |
| Secrets | api-key/webhook-secret — secret-store, ротация |
| PII | телефон/тело — Sensitive (маска, тело не логируется) |
| Tenant binding | получатель по RecipientUserId; sender — платформенный аккаунт |
Resilience И Устойчивость
| Аспект | Правило |
|---|
| Timeout | 5с к API |
| Retry | 5xx/429/timeout → перевод в Failed → повтор retry-worker-ом; invalid receiver → не retry |
| Circuit breaker | размыкание на клиенте провайдера |
| Idempotency | clientReference = NotificationId |
| Rate limit | по лимиту BSP + ChannelConfig.RateLimitPerMinute |
Обработка Callback
| Шаг | Правило |
|---|
| Verify | подпись webhook + timestamp |
| Normalize | ViberStatusWebhook → ProviderDeliveryReceipt (sent/delivered/seen/failed) |
| 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 receiver / no viber | NTF_PROVIDER_INVALID_RECIPIENT | Нет | Нет |
| не активирован (заглушка) | NTF_PROVIDER_NOT_IMPLEMENTED | Нет | Нет |
Поток доставки
Happy path и все ветки отказа отправки уведомления Channel = Viber. Статусы и переходы — по
notification.md (INV-NTF-5/7); коды — таблица «Маппинг Ошибок».
- Выбор к отправке. Dispatcher берёт свежее
Pending, retry-worker — Failed с наступившим
NextAttemptAt и Attempts < MaxAttempts; уведомление Channel = Viber. Канал выключен
(ChannelConfig.IsEnabled = false, заглушка, INV-CHN-2) → попытки нет, статус Pending, исход
NTF_PROVIDER_NOT_IMPLEMENTED (skip, метрика channel-disabled).
- Резолв адреса.
RecipientAddress.PhoneE164 (Business Messages) или MessengerHandle (public-account
подписчик) → receiver. Адрес пуст на момент попытки → уведомление ждёт апдейта реплики контактов/retry (graceful);
receiver невалиден / получатель не в Viber → NTF_PROVIDER_INVALID_RECIPIENT (non-retriable, терминальный
Failed).
- Сборка сообщения.
RenderedTitle+RenderedBody → text (≤ 1000; усечение по MaxBodyLength, если
задан, INV-CHN-3), DeepLink → keyboard-кнопка, clientReference = NotificationId (идемпотентность).
- Отправка (синхронно, timeout 5с). Повтор той же отправки (
clientReference = NotificationId) не
создаёт второе сообщение.
- Классификация ответа:
- принято BSP →
Pending/Failed → Sent, ProviderMessageId; вклад в
NotificationDispatchedEvent(Sent) (топик pin.notifications.dispatched.v1); при недоставке в Viber
возможен sms-fallback вендора — отдельный billing-исход, для агрегата это по-прежнему Sent;
5xx/timeout → NTF_PROVIDER_TRANSIENT (retriable): → Failed, Attempts++,
NextAttemptAt = now + backoff (ChannelConfig.RetryBackoff);
429 → NTF_PROVIDER_RATE_LIMITED (retriable): дефер по backoff;
- invalid receiver / no viber →
NTF_PROVIDER_INVALID_RECIPIENT (non-retriable): терминальный Failed.
- Порог повторов.
Attempts >= MaxAttempts (INV-NTF-7) → терминальный Failed +
NotificationDispatchedEvent(Failed) + алерт; запись выбывает из retry-цикла.
Обратный вызов и согласованность
Delivery/seen webhook приходит асинхронно, позже синхронного приёма BSP:
- по
clientReference = NotificationId применяется delivery-исход (sent/delivered/seen/failed →
Sent/Failed); webhook по неизвестному NotificationId игнорируется (лог).
- Повтор того же webhook идемпотентен: терминальный статус не откатывается (INV-NTF-5);
seen после
delivered не меняет Sent.
- Окно согласованности:
Sent по приёму BSP предшествует финальному webhook-у; failed-webhook может
переоткрыть уведомление в Failed и вернуть в retry-цикл.
Пред- и постусловия
- Предусловие: канал
Viber включён; есть уведомление Channel = Viber в Pending/Failed(due);
RecipientAddress.PhoneE164/MessengerHandle резолвится (или догоняется апдейтом реплики контактов/retry).
- Постусловие: уведомление в терминальном
Sent (есть ProviderMessageId) либо Failed (терминально
после MaxAttempts или invalid receiver); на финальном исходе эмитирован NotificationDispatchedEvent.
Кросс-сервисные ожидания и гарантии
| Направление | Контракт |
|---|
Ожидает от ChannelConfig (channel.md) | IsEnabled, ProviderKey, MaxAttempts, RetryBackoff, RateLimitPerMinute, MaxBodyLength |
| Ожидает от рендера шаблона | непустой RenderedBody (INV-NTF-2); опц. RenderedTitle/DeepLink |
| Ожидает от адреса | RecipientAddress.PhoneE164/MessengerHandle; пусто → ждать апдейта реплики контактов/retry, невалиден → терминальный Failed |
| Предоставляет агрегату | переход статуса Sent/Failed, ProviderMessageId, LastError (безопасный код) |
| Предоставляет наружу | вклад в NotificationDispatchedEvent (ключ партиционирования NotificationId, ContractVersion = 1) |
| Реплики | адрес — снапшот из контактной реплики RecipientContactEntity (ведётся из identity/profile); отставание реплики → адрес пуст, догоняется на retry (не потеря) |
Acceptance Criteria
| # | Given / When / Then |
|---|
| AC1 | Given канал Viber выключен, When dispatcher берёт Pending, Then попытки нет, статус Pending, метрика channel-disabled (NTF_PROVIDER_NOT_IMPLEMENTED). |
| AC2 | Given валидный receiver и канал включён, When BSP принял, 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 невалидный receiver / не в Viber, When ответ BSP, Then терминальный Failed (NTF_PROVIDER_INVALID_RECIPIENT), без повторов. |
| AC6 | Given недоставка в Viber с sms-fallback вендора, When BSP подтвердил доставку, Then для агрегата статус Sent (fallback — отдельный billing-исход). |
| AC7 | Given delivery/seen webhook по известному NotificationId, When обработка callback, Then статус приведён к webhook-исходу; повтор идемпотентен. |
Наблюдаемость
| Тип | Правило |
|---|
| Logs | send-started/sent/provider-error/webhook-received/signature-failure (телефон/тело — маска) |
| Metrics | notifications_viber_sent_total, notifications_viber_error_rate, notifications_viber_retry_total, circuit-state |
| Traces | correlationId, NotificationId, provider message id |
| Redaction | api-key/secret и raw телефон/тело — исключены |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы