04 — Реестр событий¶
Сводный каталог runtime-событий Payment Manager: actions для transactional outbox, каналы Redis pub/sub для real-time сигналов клиентам, поток push-уведомлений и заготовка под Phase 2B Redis Streams для внешних PSP-адаптеров.
Назначение¶
Этот документ — единая точка истины для всех событийных артефактов PM: что отправляется, кто продюсер, кто потребитель, в какой фазе жизненного цикла. Документ разделён по транспортным уровням:
- Outbox actions — записи в таблице
pm.outbox_event(durable, обрабатываютсяoutbox-worker). - Redis pub/sub каналы — эфемерные runtime-сигналы для real-time подписчиков (Auth Center → Flutter).
- Redis Streams (notifications) — durable push-jobs для Notifications Service (FCM worker).
- Заготовка Phase 2B — внешние Redis Streams для PSP-адаптеров (IPPS / QP / Webhook Gateway).
Связь с БД-аудитом интентов — см. секцию «Связь с intent_event».
Outbox actions¶
Таблица pm.outbox_event использует VARCHAR(20) колонку action с CHECK-ограничением. Допустимые значения (из src/shared/schema.ts, тип OutboxAction):
| action | Когда создаётся | Кто обрабатывает | Эффект |
|---|---|---|---|
post_pending |
После успешного подтверждения PSP (intent в AUTHORIZED/SETTLING) — выпуск финальных TB transfers. |
outbox-worker |
TB createTransfers с post_pending_transfer (linked chain), enrichment toName/settlementDate из psp_tx_map, опциональный PSP-fee transfer, intent → SETTLED. |
void_pending |
После отказа PSP / финального FAILED состояния (intent в AUTHORIZED, требует отката резервов). |
outbox-worker |
TB createTransfers с void_pending_transfer (linked chain), intent → FAILED с failureReason из psp_tx_map.lastError. |
Дополнительно колонка status использует свой CHECK со значениями: pending, processed, failed (тип OutboxStatus). Полную схему таблицы см. в ../database/09-outbox-event.md.
Worker role: outbox-worker — один из четырёх WORKER_ROLES PM (api, outbox-worker, psp-worker, invoice-expiry). Запускается в единственном экземпляре (см. правило в CLAUDE.md корня проекта PM: «NEVER run multiple OutboxWorker instances simultaneously»).
Жизненный цикл записи¶
created → status='pending' (insert вместе с intent в одной транзакции)
↓ outbox-worker берёт батч (ORDER BY createdAt LIMIT 10, WHERE status='pending')
↓
├─► OK → status='processed', processedAt=now()
└─► error / fatal TB-status → retryCount += 1 (status остаётся 'pending')
Финального status='failed' worker сам не выставляет — увеличивает retryCount и пробует снова на следующем тике (1 сек по умолчанию). Перевод в 'failed' — операция ручная (ops/admin), когда retryCount превышает порог и ситуация требует разбора.
После успешной обработки¶
Каждый обработчик (processPostPending / processVoidPending) после фиксации TB-операции в одной транзакции:
- Обновляет
intent.status(SETTLEDилиFAILED) — внутриdb.transaction. - Пишет settlement (только для
post_pending) черезwriteSettlement→pm.tx_history. - Помечает
outbox_event.status='processed'сprocessedAt— в той же транзакции. - Пишет аудит-запись через
writeIntentEvent→pm.intent_event(после commit). - Публикует Redis-сигнал на канал
intent.<id>(см. ниже) — best-effort. - Публикует payload в Redis Stream
stream.notifications.jobsдля FCM-доставки — best-effort.
Шаги 1–3 атомарны; шаги 4–6 — best-effort после commit. Если сервис упадёт между 3 и 4, потерянный аудит-event можно восстановить только из логов outbox-worker (info: 'outbox: intent settled').
Redis pub/sub каналы¶
PM использует Redis pub/sub для эфемерных real-time сигналов: подписчик получает сообщение, только если был онлайн в момент publish. Это контракт для Serverpod streamStatus(intentId) → Flutter app.
| Канал | Продюсер (PM) | Потребитель | Payload |
|---|---|---|---|
intent.<id> |
publishIntentStatus(intentId, status) — src/intent/intent-events.ts. Также используется outbox-worker через локальный helper publishStatus(). |
Auth Center (streamStatus) → Flutter |
{ intentId, status, updatedAt } (intent-events) / { status } (outbox-helper). |
Точки вызова publishIntentStatus:
writeIntentEvent(..., publishFn)— если переданpublishFn, вызывается асинхронно после insert вintent_event. Сбой publish — non-fatal (warn в лог, аудит-запись уже зафиксирована).outbox-worker.processPostPending—publishStatus(rec.id, 'SETTLED')после фиксации settlement.outbox-worker.processVoidPending—publishStatus(rec.id, 'FAILED')после void.
Пример payload (формат publishIntentStatus):
{
"intentId": "550e8400-e29b-41d4-a716-446655440000",
"status": "SETTLED",
"updatedAt": "2026-05-29T10:15:30.123Z"
}
Outbox-helper publishStatus отправляет упрощённый вариант {"status":"SETTLED"} — клиенту достаточно, т.к. он уже знает intentId из имени канала.
Гарантии:
- Best-effort: ошибка Redis не откатывает БД-транзакцию (см.
catchвpublishStatus— толькоlogger.warn). - Доставка не гарантирована — для durable истории используется
intent_event(см. ниже). - Подписчики обязаны делать первичный
selectтекущего статуса перед подпиской, иначе пропустят финальный SETTLED/FAILED, выпущенный до момента подписки. - Канал — Redis pub/sub (не Stream), без хранения и без consumer groups: отписался → сообщение потеряно.
Redis Streams: уведомления¶
Notifications Service (FCM push) подписан на единственный stream:
| Stream | Продюсер (PM) | Потребитель | Payload |
|---|---|---|---|
stream.notifications.jobs |
publishPaymentNotification(intent, status) — src/intent/notify.ts (XADD через выделенный notifClient). |
Notifications Service | { userId, title, body, channel: 'payment', traceId, data: {...} } |
Логика формирования payloads (src/intent/notify.ts):
- На
SETTLEDдляINVOICE_PAYMENT— два notification: покупателю (rec.userId) и мерчанту (rec.issuedByUserId). - На
SETTLEDдляINTERNAL_P2P/SERVICE_TRANSFER/ADMIN— отправителю и получателю (еслиrecipientUserIdесть вmetadata). - На
SETTLEDдля остальных операций — только инициатору. - На
FAILED— одно уведомление инициатору.
Канал доставки в payload: channel: 'payment' (это поле внутри notification, не путать с intent.channel).
Redis-клиент для notifications: если задан NOTIFICATIONS_REDIS_URL, используется отдельное соединение (см. getNotifRedis); иначе — основной Redis инстанс PM.
Заготовка на будущее: Redis Streams для PSP-адаптеров (Phase 2B)¶
Заготовка на будущее: в текущей Phase 1 PSP-логика (IPPS) выполняется in-process через
psp-worker+ PostgreSQL queue (psp_tx_map+FOR UPDATE SKIP LOCKED). Внешние Redis Streams ниже зарезервированы для Phase 2B, когда PSP-адаптеры будут вынесены в отдельные Node.js процессы. Имена и направления зафиксированы в корневом/CLAUDE.md— менять без апдейта корневого документа нельзя.
Цитата из /CLAUDE.md (раздел «Redis Streams»):
| Stream | Продюсер → Потребитель | Фаза |
|---|---|---|
stream.ipps.jobs / stream.ipps.results |
PM ↔ IPPS Adapter | Phase 2B |
stream.qp.jobs / stream.qp.results |
PM ↔ QP Adapter | Phase 2B |
stream.webhook.ipps |
Webhook Gateway → PM | Phase 2B |
stream.notifications.jobs |
PM / Auth Center / KYC → Notifications Service | Phase 1 (уже активен) |
Контракт Phase 2B (черновик):
- PM публикует в
stream.<psp>.jobsзапросы query/confirm/inquiry. - PSP-адаптер читает jobs-stream через consumer group, исполняет HTTP-вызов в PSP, публикует результат в
stream.<psp>.results. - PM читает results-stream, обновляет
psp_tx_map, инициирует outboxpost_pending/void_pending. stream.webhook.ipps— отдельный канал для асинхронных уведомлений от PSP (settlement notifications, отзывы), производитель — Webhook Gateway.
Никакого кода для этих streams в Phase 1 нет. Раздел документирует резервирование имён, чтобы adapter-команда (Phase 2B) использовала именно их.
Связь с intent_event¶
pm.intent_event — durable БД-таблица аудита жизненного цикла интента: каждая смена intent.status (включая виртуальные PSP-состояния EventStatus = IntentStatus | PspState) фиксируется одной строкой (intentId, statusFrom, statusTo, reason, payload, createdAt). Запись делает writeIntentEvent (src/intent/intent-events.ts).
Pub/sub-канал intent.<id> — эфемерный runtime-сигнал того же события для real-time UI. Pub/sub-сообщение не заменяет запись в intent_event — оно дополняет её для случаев, когда клиент подписан в момент изменения.
| Аспект | intent_event (БД) |
intent.<id> (Redis pub/sub) |
|---|---|---|
| Persistence | durable, навсегда | эфемерно, без буферизации |
| Доставка | гарантирована (insert в транзакции) | best-effort (warn при сбое) |
| Назначение | аудит, отчётность, debugging | real-time UX (streamStatus → Flutter) |
| История | полная (SELECT ORDER BY createdAt) | только текущий момент |
Полное описание таблицы и индексов — ../database/10-intent-event.md.
Связанные документы¶
../database/09-outbox-event.md— схема outbox-таблицы, индексы, retry-логика.../database/10-intent-event.md— схема аудит-таблицы.../database/07-psp-tx-map.md— состояния PSP (PspState), которые тоже могут попадать вintent_event.statusTo.- Корневой
/CLAUDE.md— раздел «Redis Streams» (источник истины для Phase 2B имён).