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

POST /api/internal/notifications/enqueue — Прямой enqueue уведомления (m2m)

Назначение

Доверенный сервис PIN, у которого нет подходящего интеграционного события, напрямую ставит уведомление в очередь. Вызывающий указывает Kind, получателей, типизированный Payload и SourceMessageId (ключ идемпотентности); сервис материализует NotificationEntity в статусе Pending тем же путём, что и консюмеры (persist-then-send), после чего dispatcher отправляет их по каналам. Owning-контекст — notifications, feature group enqueue.

Metadata

ПолеЗначение
Сервис/контекстnotifications
API areainternal (m2m, service-to-service)
Feature groupenqueue
Статусdraft
Документdocs/06-services/notifications/api/internal/enqueue-notification.md

Актор и доступ

ПроверкаЗначение
Actor typeExternalSystem (доверенный сервис PIN, m2m-токен)
Auth policy / Permissionsnotifications.enqueue (platform-scope, m2m)
Scope (RBAC)платформа (сервисный клиент)
Tenant isolationTenantId уведомления берётся из получателя (компания/null), не из вызывающего
Auditcreate (actor = вызывающий сервис)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/internal/notifications/enqueue
Success status202 (принято к отправке)
Idempotency headerIdempotency-Key = SourceMessageId (обязателен)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
SourceServicem2m claimsstringДа1..32; контекст вызывающего
SourceMessageIdheader/bodyGuidДане Guid.Empty; ключ идемпотентности
KindbodyNotificationKindДазначение enum (notification.md)
RecipientUserIdsbodyIReadOnlyList<Guid>Да1..500, непустые, уникальны
UrgencybodyNotificationUrgency?Нетdefault по Kind-политике
Localebodystring?НетBCP-47; иначе — локаль получателя
PayloadbodyPayloadModelДатипизированный union по Kind, поля соответствуют KindPII — по классификации
ChannelsbodyIReadOnlyList<Channel>?Нетограничить каналы; иначе — по настройкам получателя

Модель ответа EnqueueResultModel

ПолеТипОбязательностьИсточникMaskingОписание
EnqueuedCountintДачисло созданных NotificationEntityСколько экземпляров (получатель×канал) поставлено.
SkippedCountintДаdedup/opt-outПропущено (дубль/opt-out/нет канала).

Алгоритм

  1. Контекст и доступ. Вызывает доверенный сервис PIN (m2m-токен); требуется право notifications.enqueue. Нет валидного токена → UNAUTHENTICATED; нет права → FORBIDDEN. По ключу идемпотентности (SourceMessageId): если запрос с этим ключом уже обработан — вернуть прежний результат (202), ничего повторно не создавая.
  2. Проверка входа. Kind/Channel — допустимые значения enum; Payload соответствует заявленному Kind (типизированный union по виду); число получателей в диапазоне 1..500, идентификаторы непустые и уникальны. Нарушение → VALIDATION_FAILED.
  3. Резолв доставки (по каждому получателю). По настройкам получателя (user-notification-settings.md) определить каналы для Kind/Urgency (пересечь с Channels, если задан); подобрать шаблон (Kind, Locale, Channel); если активного шаблона под Kind/каналы нет — NTF_TEMPLATE_NOT_RESOLVED. Адрес доставки и локаль берутся снапшотом из контактной реплики recipient-contact.md (INV-RC-4/5; graceful — отсутствие реплики/точки не срывает enqueue, in-app доступен всегда); учитываются тихие часы.
  4. Материализация. Для каждой валидной пары (получатель, канал) создаётся NotificationEntity в статусе Pending с отрисованным телом; дубли по ключу дедупликации (SourceMessageId, получатель, канал, Kind) (INV-NTF-1) пропускаются.
  5. Согласованность. Созданные уведомления и отметка идемпотентности по SourceMessageId фиксируются в одном согласованном изменении; отправку по каналам выполняет dispatcher (persist-then-send). In-app уведомления доставляются realtime после фиксации.
  6. Результат. Возвращается EnqueueResultModel (EnqueuedCount, SkippedCount), HTTP 202.

Диаграмма

Побочные эффекты

ТипДетали
Внешние вызовынет синхронного lookup: адрес/локаль — из локальной контактной реплики RecipientContactEntity
Уведомлениясозданные NotificationEntity (Pending); отправку по каналам и dispatched-факт эмитит dispatcher

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
UNAUTHENTICATED401нет/невалидный m2m-токен«Требуется авторизация»no retry
FORBIDDEN403нет notifications.enqueue«Недостаточно прав»no retry
VALIDATION_FAILED400Kind/Payload/получатели вне ограничений«Проверьте запрос»no retry
NTF_TEMPLATE_NOT_RESOLVED422нет Active-шаблона для Kind/каналов«Шаблон не настроен»no retry (настроить шаблон)

Частичный результат и предельные входы

  • Per-получатель, per-канал. Резолв независим по каждому получателю: каналы фильтруются его настройками (opt-out, GlobalMute, Urgency), для канала подбирается тело шаблона. EnqueuedCount — число созданных NotificationEntity, SkippedCount — сумма пропусков: dedup-дубль (INV-NTF-1), opt-out канала (INV-UNS-3), отсутствие тела включённого канала (INV-TPL-5), тихие часы не пропуск (канал ставится с ScheduledAt).
  • Отсутствие шаблона Kind. Если для Kind вообще нет Active-шаблона (ни в запрошенной локали, ни в fallback ru) — весь запрос отклоняется NTF_TEMPLATE_NOT_RESOLVED (422), ничего не создаётся (админ должен завести/опубликовать шаблон). Если шаблон есть, но у части каналов нет тела — эти каналы просто пропускаются (в SkippedCount).
  • Все каналы получателя отключены / только Critical-канал при не-Critical Kind → получатель пропущен (в SkippedCount), это не ошибка запроса.
  • Границы. RecipientUserIds 1..500, непустые и уникальны (дубликаты в списке схлопываются, повтор не создаёт двойного уведомления благодаря dedup-ключу). Пустой список → VALIDATION_FAILED.
  • Тихие часы / срочность. Interruptive-каналы в окне тишины при Urgency != Critical получают ScheduledAt = открытие окна (INV-NTF-4); Critical игнорирует тихие часы и GlobalMute (INV-UNS-4).

Идемпотентность и конкурентность

  • Ключ повтораSourceMessageId (= Idempotency-Key). Повторный вызов с тем же ключом возвращает прежний EnqueueResultModel (202), ничего не досоздавая. Вызывающий обязан сохранять SourceMessageId стабильным между своими ретраями (иначе повтор породит новый набор уведомлений).
  • Гонка двух одинаковых вызовов (тот же SourceMessageId) — один материализует набор, второй упирается в отметку идемпотентности / dedup-уникальность (SourceMessageId, RecipientUserId, Channel, Kind) и не создаёт дублей; оба получают согласованный результат.

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

  • Предусловие. Валидный m2m-токен с notifications.enqueue; Payload типизирован под Kind; для Kind существует Active-шаблон; RecipientUserIds в границах.
  • Постусловие (успех). Для каждой разрешённой пары (получатель, канал) создан NotificationEntity (Pending, при тихих часах — с ScheduledAt); отметка идемпотентности по SourceMessageId зафиксирована в том же согласованном изменении; in-app доставлено realtime; отправку внешних каналов подхватит dispatcher.

Кросс-сервисные контракты

  • Ожидает от вызывающего: стабильный SourceMessageId (Guid, ключ идемпотентности), Payload, соответствующий Kind (дом полей — event-контракт сервиса-источника), корректные RecipientUserId.
  • Ожидает от identity/profile: событийную реплику контактов (RecipientContactEntity, ведётся из pin.identity.users.v1 — ФИО/телефон/статус; email/локаль — forward из контактного события profile) — синхронного lookup адреса при enqueue нет. Отсутствие реплики/точки — graceful: уведомление создаётся, внешний адрес догоняется после апдейта реплики/адаптером (in-app от адреса не зависит).
  • Предоставляет: синхронный ответ 202 с EnqueuedCount/SkippedCount; далее — тот же путь доставки, что у консюмеров (dispatcher, pin.notifications.dispatched.v1 при финальном исходе).

Совместимость и наблюдаемость

  • Новые Kind/каналы — additive. SourceMessageId обязателен для идемпотентности повторов.
  • Logs: enqueue-received/enqueued (SourceService, Kind, count); metrics: notifications_enqueue_api_total{source,kind}, latency p95 < 1 с.

Acceptance criteria

  • Given валидный сервис с notifications.enqueue, Active-шаблон и 3 получателя, when enqueue с корректным Payload, then 202; EnqueuedCount = число разрешённых пар получатель×канал, Pending созданы, in-app доставлены realtime.
  • Given тот же SourceMessageId повторно, when enqueue, then 202 с прежним результатом, новых уведомлений нет.
  • Given часть получателей с opt-out канала, when enqueue, then они учтены в SkippedCount, запрос успешен.
  • Given для Kind нет Active-шаблона (даже fallback ru), when enqueue, then 422 NTF_TEMPLATE_NOT_RESOLVED, ничего не создано.
  • Given Payload не соответствует Kind или RecipientUserIds пуст/‌> 500, when enqueue, then 400 VALIDATION_FAILED.
  • Given невалидный m2m-токен, when enqueue, then 401 UNAUTHENTICATED; при валидном токене без права — 403 FORBIDDEN.
  • Given получатель в тихих часах и Urgency = Normal, when enqueue interruptive-канала, then уведомление создано с ScheduledAt = открытие окна (не отправлено немедленно); при Urgency = Critical — отправляется сразу.

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

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

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