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

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

Назначение

Доставка канала Max (мессенджер MAX) (../domain/channel.md) через Bot/Business API MAX. Заглушка: реализация отложена — контракт зафиксирован, отправка активируется per-канал флагом. Привязку UserId → MAX chat-id ведёт адаптер (deep-link /start); уведомление хранит только RecipientUserId. Provider input (updates) — недоверенный внешний ввод.

Границы Adapter-а

ПолеЗначение
ProviderMAX Bot/Business API
Сервисnotifications
Capabilitymessaging (max)
Adapter classMaxChannelProvider
Typed clientIMaxBotClient + IMessengerHandleStore (chat-id по UserId)
Внутренняя операцияdispatcher при Channel = Max, Status = Pending
Реализациязаглушка (отложено) — до включения канала отправка не выполняется и возвращается код NTF_PROVIDER_NOT_IMPLEMENTED

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

ОперацияDirectionInternal modelProvider-specific model
sendMessageoutboundNotificationDispatchRequestMaxSendMessage (chat_id/text/buttons)
bot updates (/start, block)callbackпривязка / отвязка messenger-handleMaxUpdate (подписанный webhook)

Модель Internal NotificationDispatchRequest

Дом — ../domain/notification.md. Ключевые: NotificationId (idempotency), RecipientUserId → chat-id из IMessengerHandleStore, RenderedTitle+RenderedBodytext, DeepLink → кнопка.

Модель Provider-Specific MaxSendMessage

ПолеТипОбязательностьProvider formatValidationInternal mapping
chat_idstringДаMAX chat idпривязка есть (иначе skip)IMessengerHandleStore.Get(RecipientUserId)
textstringДаtext/markupне пустоеRenderedTitle+RenderedBody
buttonsобъект?Нетinline buttonsDeepLink
clientReferencestringДаidempotency= NotificationIdNotificationId

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

КонтрольПравило
Authenticationbot/business-token MAX
Signature validationwebhook — подпись/secret-token + timestamp
Secretsтокены/webhook-secret — secret-store, ротация
PIIтело — Sensitive (маска); chat-id — Sensitive, не логируется
Tenant bindingпривязка chat-id к RecipientUserId; отправка только в привязанный chat

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

АспектПравило
Timeout5с к API
Retry5xx/429/timeout → перевод в Failed → повтор retry-worker-ом; blocked → unlink, не retry
Circuit breakerразмыкание на клиенте провайдера
IdempotencyclientReference = NotificationId
Rate limitпо лимиту MAX + ChannelConfig.RateLimitPerMinute

Обработка Callback

ШагПравило
Verifyподпись/secret webhook + timestamp
NormalizeMaxUpdate → привязка handle (/start с UserId) / отвязка
Validatedeep-link payload = валидный UserId
Dispatchпривязка/отвязка chat-id в IMessengerHandleStore
Storehandle в store; статус доставки — на NotificationEntity

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

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

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

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

  1. Выбор к отправке. Dispatcher берёт свежее Pending, retry-worker — Failed с наступившим NextAttemptAt и Attempts < MaxAttempts; уведомление с Channel = Max. Если канал выключен (ChannelConfig.IsEnabled = false — заглушка не активирована, INV-CHN-2) → попытка не выполняется, статус остаётся Pending, исход NTF_PROVIDER_NOT_IMPLEMENTED (skip, метрика channel-disabled).
  2. Резолв chat-id. Адрес канала — chat-id, привязанный к RecipientUserId (реестр привязок ведёт адаптер). Привязки нет → NTF_MESSENGER_NOT_LINKED: канал пропускается (non-retriable, без бесконечных повторов); получатель покрыт другими разрешёнными каналами (in-app доставляется всегда).
  3. Сборка сообщения. RenderedTitle+RenderedBodytext (усечение по ChannelConfig.MaxBodyLength, если задан, INV-CHN-3), DeepLink → inline-кнопка, clientReference = NotificationId (идемпотентность у провайдера).
  4. Отправка (синхронно, timeout 5с). Повторная отправка того же NotificationId не создаёт второе сообщение (идемпотентность по clientReference).
  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;
    • bot blocked → NTF_MAX_BLOCKED (non-retriable): терминальный Failed, привязка chat-id снимается (unlink) — до повторного /start канал недоставляем.
  6. Порог повторов. Attempts >= MaxAttempts (INV-NTF-7) → терминальный Failed + NotificationDispatchedEvent(Failed) + алерт; запись выбывает из retry-цикла (ручной разбор по runbook terminal-failures).

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

Bot-updates приходят асинхронно и не привязаны к конкретной отправке — это привязка/отвязка handle, не delivery-receipt:

  • /start c deep-link payload UserId → после проверки подписи/secret-token и валидности UserId chat-id привязывается; с этого момента канал Max доставляем.
  • block/stop → отвязка chat-id; последующие отправки до повторного /start завершаются NTF_MESSENGER_NOT_LINKED.
  • Повтор того же update идемпотентен (привязка к тому же chat-id — no-op); update с неизвестным/невалидным UserId игнорируется (лог webhook-received, без ошибки).
  • Окно согласованности: между отправкой и block возможна гонка — сообщение, ушедшее до обработки block, считается Sent; отвязка влияет только на будущие отправки.

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

  • Предусловие: канал Max включён (IsEnabled = true); есть уведомление Channel = Max в Pending/Failed(due); для доставки — привязан chat-id (/start).
  • Постусловие: уведомление в терминальном Sent (доставлено, есть ProviderMessageId) либо Failed (терминально после MaxAttempts или non-retriable unlink); на финальном исходе эмитирован NotificationDispatchedEvent. При отсутствии привязки канал пропущен, прочие каналы получателя не затронуты.

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

НаправлениеКонтракт
Ожидает от ChannelConfig (channel.md)IsEnabled, ProviderKey, MaxAttempts, RetryBackoff, RateLimitPerMinute, MaxBodyLength
Ожидает от рендера шаблонанепустой RenderedBody (INV-NTF-2); опц. RenderedTitle/DeepLink
Ожидает от привязкиchat-id по RecipientUserId (deep-link /start); отсутствует → skip
Предоставляет агрегатупереход статуса Sent/Failed, ProviderMessageId, LastError (безопасный код)
Предоставляет наружувклад в NotificationDispatchedEvent (ключ партиционирования NotificationId, ContractVersion = 1)
Репликисвоих реплик нет; недоступность провайдера — transient-retry, не потеря уведомления

Acceptance Criteria

#Given / When / Then
AC1Given канал Max выключен, When dispatcher берёт Pending, Then попытки нет, статус Pending, метрика channel-disabled (NTF_PROVIDER_NOT_IMPLEMENTED).
AC2Given привязан chat-id и канал включён, When отправка успешна, Then Sent + ProviderMessageId + вклад в NotificationDispatchedEvent(Sent).
AC3Given провайдер вернул 5xx/429, When обработка ответа, Then Failed, Attempts++, NextAttemptAt по backoff; повтор до успеха или MaxAttempts.
AC4Given MaxAttempts исчерпан, When очередная ошибка, Then терминальный Failed + NotificationDispatchedEvent(Failed) + алерт.
AC5Given bot заблокирован, When ответ blocked, Then терминальный Failed + unlink chat-id (NTF_MAX_BLOCKED), без повторов.
AC6Given нет привязки chat-id, When отправка, Then канал пропущен (NTF_MESSENGER_NOT_LINKED), in-app доставлен независимо.
AC7Given повтор той же отправки (NotificationId), When идемпотентный clientReference, Then у провайдера одно сообщение (без дубля).

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

ТипПравило
Logssend-started/sent/blocked/not-linked/provider-error/webhook-received (тело/chat-id — маска)
Metricsnotifications_max_sent_total, notifications_max_error_rate, notifications_max_linked_total, circuit-state
TracescorrelationId, NotificationId, provider request id
Redactionтокены/secret и raw тело/chat-id — исключены

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

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

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