Consumer: Wallet Events → Notifications (notifications-wallet-events)
Маппит события кошелька (pin.wallet.transaction-recorded.v1 — проводки холдерских счетов) в
NotificationKind и ставит уведомления в очередь доставки. Дом payload —
wallet/events/wallet-transaction-recorded.md;
целевой агрегат — notification.md; общий алгоритм постановки в очередь —
crm-events.md.
Назначение
Consumer group notifications-wallet-events уведомляет холдера счёта о движении его внутреннего баланса:
«вам начислено», «вам выплачено», «баланс скорректирован». Денежных операций не выполняет — только
оповещает о фактах журнала кошелька. persist-then-send; кросс-контекстной записи нет.
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | notifications |
| Consumer group | notifications-wallet-events |
| Producer | wallet |
| Topic | pin.wallet.transaction-recorded.v1 (key = AccountId — строгий порядок per-account, BalanceAfter монотонно согласован) |
| ContractVersion | ожидается 1; несовпадение — warn-and-proceed |
Маппинг событий
OperationKind / Direction | → NotificationKind | Текст (суть) | Получатель | Urgency |
|---|---|---|---|---|
CommissionCredit, ReferralCredit | WalletCredited | «вам начислено {Amount}, баланс {BalanceAfter}» | холдер (HolderUserId); для AccountKind = AgencyCommission — руководитель агентства HolderCompanyId (роль с rewards.payouts.manage) | Normal |
PayoutDebit | WalletDebited | «вам выплачено {Amount}» | холдер / руководитель агентства | Normal |
Adjustment, Correction | WalletAdjusted | «баланс скорректирован ({Direction})» | холдер / руководитель агентства | Normal |
Поля уведомления — WalletNotificationPayload (notification.md).
Контекст события
- Actor = система; события системных счетов в топик не публикуются — каждый входящий факт адресуем холдеру.
- Идемпотентность: consumer-level по ключу
notifications-wallet:{MessageId}; business-level — dedup-unique (INV-NTF-1) + уникальныйEntryIdпроводки.
Входные данные / payload события
| Поле payload | Тип | Обяз. | Как используется |
|---|---|---|---|
EntryId / TransactionId | Guid | Да | дедупликация; ссылка в WalletNotificationPayload |
AccountId, AccountKind | Guid / enum | Да | Account { Id, Kind } payload; выбор адресата |
HolderUserId / HolderCompanyId | Guid? | XOR | получатель fan-out (пользователь / руководитель агентства) |
OperationKind, Direction | enum | Да | выбор NotificationKind и шаблона |
Amount, BalanceAfter, CurrencyId + CurrencySnapshot | decimal / snapshot | Да | суммы в тексте (форматирование по валюте) |
AccountVersion | long | Да | монотонный guard порядка per-account |
Алгоритм
Общая логика постановки в очередь — crm-events.md:
- Восстановить контекст события и сверить
ContractVersion(несовпадение — warn-and-proceed); проверить идемпотентность по идентификатору сообщения — дубль пропускается (ack, no-op). - Проверить payload (
Amount > 0,EntryId/AccountIdне пусты, холдер задан — XOR); при нарушении —NTF_INVALID_PAYLOAD(в DLQ). - Определить получателя:
HolderUserId; дляAgencyCommission— руководитель агентстваHolderCompanyId(резолв черезidentity, роль сrewards.payouts.manage); применить дедуп. - Определить каналы по виду и срочности, резолвить шаблон и адрес (мягкая деградация при отсутствии);
применить тихие часы. Шаблон не резолвится —
NTF_TEMPLATE_NOT_RESOLVED(в DLQ). - Создать уведомление в статусе
Pending(dedup по ключу fan-out, INV-NTF-1); уведомление и отметка идемпотентности — в одном согласованном изменении (persist-then-send). - Успех → ack; retriable-сбой → retry; non-retriable → DLQ.
Идемпотентность и replay
- Consumer-level по
MessageId/EntryId; business-level — dedup-unique (INV-NTF-1). Replay: да.
Retry и DLQ
| Сбой | Код | Retry | DLQ |
|---|---|---|---|
| Инфраструктурный / резолв руководителя агентства недоступен | — | backoff + пауза партиции | Нет |
| Нарушение payload | NTF_INVALID_PAYLOAD | Нет | Да + алерт |
| Шаблон не резолвится | NTF_TEMPLATE_NOT_RESOLVED | Нет | Да + алерт |
Наблюдаемость и безопасность
- Logs:
consumed/skipped-duplicate/no-recipients/enqueued/failed. - Metrics:
notifications_enqueued_total{kind,channel},notifications_consumer_lag_seconds{source=wallet}. - Безопасность: system-actor; суммы и балансы — Sensitive-класс (наружу только холдеру/руководителю, в логах маскируются); m2m ACL Kafka.
Порядок, версии и неизвестные события
- Порядок per-account гарантирован ключом топика; «поздний» повтор события со старым
AccountVersionвреда не несёт (уведомление по-событийное, дедуп поEntryId). - Неизвестное значение
OperationKind/AccountKind/Direction→ не маппится →ignored-event(ack), лог; версия ниже/выше1— warn-and-proceed по известным полям.
Постусловие
- Холдеру (или руководителю агентства) по каждому разрешённому каналу создан ровно один
NotificationEntity(Pending, Kind = WalletCredited | WalletDebited | WalletAdjusted); тело содержит сумму и баланс, отформатированные по валюте; offset подтверждён после фиксации. - Обратной записи в
walletи денежных операций не выполнялось.
Acceptance criteria
- Given
transaction-recordedсOperationKind = CommissionCreditиHolderUserId, When consumer обрабатывает, Then холдеру создаётсяNotificationEntity(Pending, Kind = WalletCredited)с суммой иBalanceAfter. - Given событие счёта
AgencyCommission, When обработка, Then уведомление адресовано руководителю агентстваHolderCompanyId; при недоступности резолва — retry (backoff). - Given повторная доставка того же события (
EntryIdсовпадает), When обработка, Thenskipped-duplicate(ack), дублей нет. - Given событие с неизвестным
OperationKind, When маппинг, Thenignored-event(ack), лог.
Обратные ссылки
Автоссылки: где используется этот документ.
- События (1): Event Contract: pin.wallet.transaction-recorded (WalletTransactionRecordedKafkaEvent)
- index.md (1): Notifications Service
Связанные документы
- Целевой агрегат: notification.md; общий алгоритм — crm-events.md; входной контракт — wallet/events/wallet-transaction-recorded.md; карта — README контекста.
- Источники движения баланса: wallet/domain/ledger-transaction.md (карта проводок), wallet/domain/wallet-account.md.
- ADR: 0065, 0056.