Consumer: Rewards Referral Accrual Status Changed
Назначение
Consumer слушает статусные события реферальных начислений (топик pin.rewards.referral-accruals.v1) и
ведёт баланс счёта ReferralCommission партнёра: переход начисления в Approved (родительское
комиссионное начисление прошло гейт получения денег, ADR-0067)
кредитует счёт транзакцией ReferralCredit; отмена начисления после Approved компенсирует ранее
проведённый кредит транзакцией Correction. Дебет при выплате — не здесь: его проводит consumer
поручений (rewards-payout-order-status-changed.md).
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | wallet |
| Обрабатываемое событие | pin.rewards.referral-accrual-status-changed (ReferralAccrualStatusChangedKafkaEvent); соседний тип топика ReferralAccrualCreatedKafkaEvent — пропускается (ack, no-op) |
| Topic | pin.rewards.referral-accruals.v1 (key = id начисления — строгий порядок переходов per-начисление) |
| ContractVersion | 1; при несовпадении версии — warn-and-proceed |
| Consumer group | pin-wallet-rewards-referral-accruals |
| Producer | rewards: авто-апрув при регистрации акта (../../rewards/domain/platform-receipt.md), отмена начисления (../../rewards/domain/referral-accrual.md) |
| Эффект (событие → что меняется) | транзакции ReferralCredit / Correction в журнале (../domain/ledger-transaction.md); баланс счёта ReferralCommission партнёра |
Контекст события
- Из метаданных доступны: идентификатор сообщения (дедупликация),
AggregateId= id начисления, время события; корреляция трейса — из метаданных. - Обработка — под системным актором вне tenant-контекста (реферальные начисления — платформенные).
- Ключи бизнес-идемпотентности: кредит —
(ReferralCredit, SourceId = Id начисления); компенсация —(Correction, SourceId = Id исходной ReferralCredit-транзакции).
Входные данные / payload события
Контракт — ../../rewards/events/referral-accrual-status-changed.md. Ниже — поля, которые consumer читает.
| Поле payload | Тип | Обязательность | Как используется (маппинг) |
|---|---|---|---|
Id | Guid | Да | Id реферального начисления → SourceId кредита; поиск исходной транзакции при отмене; = Kafka key |
PartnerUserId | Guid | Да | Холдер счёта ReferralCommission |
FromStatus / ToStatus | string (enum ReferralAccrualStatus) | Да | Выбор эффекта (таблица ниже) |
Amount | Money (Value + CurrencyId + CurrencySnapshot) | Да | Сумма кредита; при компенсации сумма берётся из исходной транзакции (зеркально) |
CancelReason | string? (enum ReferralCancelReason) | Да при Cancelled | Причина — фиксируется в Correction-транзакции (аудит) |
ApprovedAt / PaidAt | DateTimeOffset? | Нет | ApprovedAt → OccurredAt кредита |
Маппинг «переход → эффект»
ToStatus | Эффект | Проводки |
|---|---|---|
Approved | Транзакция ReferralCredit (SourceId = Id начисления) | Дебет PlatformSettlement → кредит ReferralCommission партнёра на Amount |
Cancelled, кредит был проведён (отмена после Approved) | Компенсирующая транзакция Correction (SourceId = CorrectedTransactionId = id исходной ReferralCredit-транзакции; причина CancelReason — в Comment) | Зеркально исходной: дебет ReferralCommission партнёра → кредит PlatformSettlement на сумму исходной транзакции |
Cancelled, кредита не было (отмена из Pending) | No-op (ack) — деньги в кошелёк не попадали | — |
Paid | No-op (ack) — дебет проводится consumer-ом поручений по позиции (PayoutDebit), не статусным событием | — |
| неизвестное значение | DLQ + алерт (WALLET_UNKNOWN_ENUM): переход с возможным денежным эффектом не распознан — молча не ack-аем, чтобы проводка не потерялась | — |
Компенсация при отмене после Approved штатно не встречает недостатка баланса: отмена в rewards
допустима только пока начисление не включено в поручение
(../../rewards/domain/referral-accrual.md, INV-RA-5), то есть
выплата не проводилась. Если баланс всё же меньше суммы компенсации (INV-WLT-3,
WALLET_BALANCE_NEGATIVE_FORBIDDEN) — это детектор рассинхрона: не проводить, DLQ + алерт (ADR-0021).
Алгоритм
- Контекст события. Дедупликация по идентификатору сообщения;
ContractVersion— warn-and-proceed; событие типаReferralAccrualCreatedKafkaEvent→ ack, no-op. - Выбор эффекта. По
ToStatus— таблица маппинга;Approved→ шаг 3,Cancelled→ шаг 4,Paidи прочие известные без эффекта → ack, no-op. Неизвестное значениеToStatus(не входит в enumReferralAccrualStatus) → non-retriable, DLQ + алерт (WALLET_UNKNOWN_ENUM): переход с потенциальным денежным эффектом не распознан, молчаливый ack запрещён (проводка не должна теряться). - Кредит (
Approved). Идемпотентность: транзакция(ReferralCredit, Id начисления)уже есть → дубль, ack (INV-WLT-7). ПроверитьCurrencyId(RUB-only — иначе DLQWALLET_CURRENCY_MISMATCH, INV-WLT-4),Amount.Value > 0. Получить-или-создать счётReferralCommission(HolderUserId = PartnerUserId; гонка lazy-создания — по уникальности «холдер + вид», INV-WLT-2). ПровестиReferralCredit: дебетPlatformSettlement, кредит счёта партнёра,OccurredAt = ApprovedAt. - Компенсация (
Cancelled). Найти проведённуюReferralCredit-транзакцию поSourceId = Idначисления: нет → кредита не было, ack (no-op). Есть → идемпотентность:(Correction, id исходной транзакции)уже есть → дубль, ack (INV-WLT-6). Баланс счёта партнёра меньше суммы исходной транзакции → DLQ + алерт (WALLET_BALANCE_NEGATIVE_FORBIDDEN, рассинхрон). Иначе провестиCorrectionзеркально исходной (CorrectedTransactionId) с причинойCancelReasonвComment. - Какие факты фиксируются. По каждой проведённой транзакции публикуется
pin.wallet.transaction-recorded.v1(../events/wallet-transaction-recorded.md) — потребительnotifications(«вам начислено» / «начисление отменено»). - Согласованность. Транзакция, балансы и исходящее событие — в одном согласованном изменении; offset подтверждается после фиксации.
- Результат. Успех → ack; контролируемая ошибка → retry/DLQ по политике ниже.
Идемпотентность и replay
- Business keys:
(ReferralCredit, Id начисления)и(Correction, id исходной транзакции)— unique в журнале; дубль любого события — no-op. - Порядок: key топика = id начисления —
Approvedвсегда раньшеCancelled-после-апрува; перестановки между разными начислениями безразличны. - Version-guard: warn-and-proceed по
ContractVersion; неизвестные additive-поля игнорируются; монотонность переходов дополнительно страхуетсяVersionpayload (устаревший повтор упирается в идемпотентность). - Replay: да, безопасен — кредиты и компенсации пропускаются по unique-ключам. Событие с неизвестным
ToStatusпри replay детерминированно попадает в DLQ + алерт (а не тихо ack-ается), пока эффект перехода не определён и маппинг/версия не дотянуты.
Retry и DLQ
| Сбой | Retry | DLQ | Ручное действие |
|---|---|---|---|
| Транзиентная ошибка хранилища | экспоненциальный retry | после исчерпания — да | replay после восстановления |
Валюта вне допустимых (WALLET_CURRENCY_MISMATCH, INV-WLT-4) | нет | Да | разбор контракта (ADR-0048) |
Баланс партнёра < суммы компенсации (WALLET_BALANCE_NEGATIVE_FORBIDDEN, INV-WLT-3) | нет | Да + алерт | reconciliation-issue (ADR-0021): сверка rewards ↔ wallet, исправление командой rewards либо Adjustment |
Amount.Value <= 0 при Approved / отсутствие обязательных полей | нет | Да | разбор контракта с rewards |
Неизвестное значение ToStatus (не в enum ReferralAccrualStatus) (WALLET_UNKNOWN_ENUM) | нет | Да + алерт | разбор контракта с rewards: определить эффект перехода, дотянуть маппинг/версию, ручной replay из DLQ после фикса |
Неизвестный ToStatus — non-retriable DLQ + алерт (WALLET_UNKNOWN_ENUM), единообразно с
commission-money-received (неизвестный enum → DLQ): переход с возможным денежным эффектом не проводится
и не ack-ается молча, а разбирается вручную (см. маппинг и Алгоритм, шаг 2).
Наблюдаемость и безопасность
- Logs: consumed / skipped-duplicate / posted / compensated / failed —
Idначисления,PartnerUserId, id транзакции,CancelReason, без сумм. Metrics:wallet_transactions_recorded_total{kind="ReferralCredit"|"Correction"},wallet_unknown_status_total(каждый инкремент = событие в DLQ + алерт, целевое значение 0), DLQ, lag. - Traces: span приёма → span проведения; корреляция из метаданных события.
- PII нет (
InvestorUserIdв событии отсутствует); суммы — Sensitive-класс.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: LedgerTransactionEntity (+ LedgerEntryEntity)
- API (3): GET /api/v1/my/wallet — Мой кошелёк (балансы счетов), POST /api/v1/platform/wallet/adjustments — Ручная корректировка счёта, Wallet API — реестр операций и общие контрактные модели
- Use Cases (1): UC-WLT-001. Получение денег кредитует балансы получателей
- События (3): Event Contract: pin.rewards.referral-accrual-created (ReferralAccrualCreatedKafkaEvent), Event Contract: pin.rewards.referral-accrual-status-changed (ReferralAccrualStatusChangedKafkaEvent), Event Contract: pin.wallet.transaction-recorded (WalletTransactionRecordedKafkaEvent)
- Консюмеры (1): Consumer: Rewards Payout Order Status Changed
- index.md (1): Wallet Service
Связанные документы
- Контракты входных событий: ../../rewards/events/referral-accrual-status-changed.md, ../../rewards/events/referral-accrual-created.md (пропускается); сущность-источник: ../../rewards/domain/referral-accrual.md.
- Дебет при выплате: rewards-payout-order-status-changed.md.
- Целевые сущности: ../domain/wallet-account.md, ../domain/ledger-transaction.md; исходящее событие: ../events/wallet-transaction-recorded.md.
- ADR-0065, ADR-0021.