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

Consumer: Rewards Commission Distribution Adjusted

Назначение

Consumer слушает событие корректировки распределения комиссионного начисления (pin.rewards.commission-distribution-adjusted.v1 — публикуется, когда руководитель агентства меняет доли Agency/Agent по начислению, уже прошедшему гейт получения денег, ADR-0067) и выравнивает балансы получателей под новое деление пула: проводит в журнале кошелька транзакцию Adjustment, переносящую дельту между холдерскими счетами AgentCommission и AgencyCommission. Первичный кредит по этому начислению уже проведён consumer-ом получения денег (rewards-commission-money-received.md) по старому распределению; данный consumer лишь довносит разницу. Строка Platform и суммарный баланс получателей не меняются — дельта переносится с одного холдерского счёта на другой.

Owning-сервис и триггер

ПолеЗначение
Сервис/контекстwallet
Обрабатываемое событиеpin.rewards.commission-distribution-adjusted (CommissionDistributionAdjustedKafkaEvent)
Topicpin.rewards.commission-distribution-adjusted.v1 (key = AccrualId — строгий порядок корректировок per-начисление)
ContractVersion1; при несовпадении версии — warn-and-proceed
Consumer grouppin-wallet-rewards-distribution-adjusted
Producerrewards: per-deal корректировка распределения в статусе MoneyReceived (../../rewards/api/agency/adjust-accrual-distribution.md) — по одному событию на каждую корректировку
Эффект (событие → что меняется)одна транзакция Adjustment в журнале (../domain/ledger-transaction.md): перенос дельты между счетами AgentCommission и AgencyCommission; балансы обоих счетов

Контекст события

  • Из метаданных доступны: идентификатор сообщения (дедупликация), AggregateId = AccrualId, время события; корреляция трейса — из метаданных.
  • Обработка — под системным актором вне tenant-контекста: счета кошелька платформенные, холдер-based; причина корректировки (Reason) фиксируется в Comment транзакции как аудит-след.
  • Ключ бизнес-идемпотентности: транзакция (OperationKind = Adjustment, SourceId), где SourceId детерминированно выводится из пары (AccrualId, Version) события — каждая корректировка (со своим Version) порождает не более одной Adjustment-транзакции (INV-WLT-7).

Входные данные / payload события

Контракт — ../../rewards/events/commission-distribution-adjusted.md. Ниже — поля, которые consumer читает.

Поле payloadТипОбязательностьКак используется (маппинг)
AccrualIdGuidДаНачисление → часть SourceId транзакции (с Version); = Kafka key
TenantIdGuidДаАгентство → холдер счёта AgencyCommission (HolderCompanyId)
AgentUserIdGuidДаАгент → холдер счёта AgentCommission (HolderUserId)
OldAgencyAmount / NewAgencyAmountMoney (Value + CurrencyId + CurrencySnapshot)ДаДельта агентства NewAgencyAmount − OldAgencyAmount
OldAgentAmount / NewAgentAmountMoneyДаДельта агента NewAgentAmount − OldAgentAmount (= −дельта агентства)
ReasonstringДаПричина корректировки → Comment транзакции (аудит)
VersionlongДаДоменная версия начисления → часть SourceId (идемпотентность), монотонный guard

Маппинг «дельта → проводка»

Одна транзакция Adjustment на событие; внутри — две проводки двойной записью между холдерскими счетами (Σ дебетов = Σ кредитов, ../domain/ledger-transaction.md, INV-WLT-5). Пусть Δ = NewAgentAmount.Value − OldAgentAmount.Value (дельта агента; дельта агентства = −Δ):

Знак ΔСмыслДебетКредитСумма
Δ > 0доля агента выросла, агентства — уменьшиласьAgencyCommission холдера TenantIdAgentCommission холдера AgentUserId`
Δ < 0доля агента уменьшилась, агентства — вырослаAgentCommission холдера AgentUserIdAgencyCommission холдера TenantId`
Δ = 0доли не изменились (суммы совпали)No-op (ack): переносить нечего

PlatformSettlement, PlatformRevenue и строка Platform в переносе не участвуют — дельта остаётся внутри пула агентства.

Алгоритм

  1. Контекст события. Дедупликация по идентификатору сообщения; ContractVersion — warn-and-proceed. Проверить бизнес-идемпотентность: транзакция (Adjustment, SourceId из AccrualId + Version) уже проведена → пропустить как дубль (ack, no-op, INV-WLT-7).
  2. Проверка входа. Единый CurrencyId всех сумм, допустимый платформой (RUB-only, ADR-0048) — иначе DLQ (WALLET_CURRENCY_MISMATCH, INV-WLT-4). Сохранение пула (следствие INV-11 producer-а): (NewAgencyAmount − OldAgencyAmount) + (NewAgentAmount − OldAgentAmount) = 0 — иначе DLQ + алерт (WALLET_DISTRIBUTION_SUM_MISMATCH: контракт producer-а нарушен, перенос увёл бы Σ балансов). Дельта Δ = 0 → No-op (ack).
  3. Резолв счетов. Счета AgentCommission (HolderUserId = AgentUserId) и AgencyCommission (HolderCompanyId = TenantId) уже существуют (кредитованы при получении денег); при их отсутствии — рассинхрон (WALLET_ACCOUNT_NOT_FOUND / детектор покрытия) → DLQ + алерт.
  4. Проведение. Одна транзакция Adjustment (SourceId из AccrualId + Version, Comment = Reason, ActorUserId = null — системная авто-компенсация) с двумя проводками по таблице маппинга; пересчёт балансов обоих счетов. Дебетуемый счёт не может уйти в минус (INV-WLT-3): если у дебетуемой стороны баланс меньше |Δ| (доля уже израсходована выплатой) — не проводить, DLQ + алерт (WALLET_BALANCE_NEGATIVE_FORBIDDEN, детектор рассинхрона, а не бизнес-отказ — ADR-0021).
  5. Какие факты фиксируются. По каждой проводке холдерского счёта публикуется pin.wallet.transaction-recorded.v1 (../events/wallet-transaction-recorded.md, key = AccountId) — потребитель notifications («ваша доля скорректирована»).
  6. Согласованность. Транзакция, обе проводки, балансы и исходящие события — в одном согласованном изменении; offset подтверждается после фиксации.
  7. Результат. Успех → ack; контролируемая ошибка → retry/DLQ по политике ниже.

Диаграмма

Идемпотентность и replay

  • Business key: unique (OperationKind = Adjustment, SourceId из AccrualId + Version); дубль — пропуск (no-op, INV-WLT-7). Каждая корректировка со своим Version — отдельная транзакция.
  • Порядок: key топика = AccrualId — последовательные корректировки приходят по возрастанию Version; устаревший повтор упирается в идемпотентность.
  • Version-guard: несовпадение ContractVersion — warn-and-proceed; неизвестные additive-поля игнорируются.
  • Replay: да, безопасен — уже проведённые переносы пропускаются по unique-ключу; полный replay топика восстанавливает переносы без задвоения.

Retry и DLQ

СбойRetryDLQРучное действие
Транзиентная ошибка хранилищаэкспоненциальный retryпосле исчерпания — даreplay после восстановления
Валюта вне допустимых (WALLET_CURRENCY_MISMATCH, INV-WLT-4)нетДаразбор контракта (ADR-0048)
Пул не сохранён — Σ дельт ≠ 0 (WALLET_DISTRIBUTION_SUM_MISMATCH)нетДа + алертинцидент producer-а: сверка с начислением в rewards (ADR-0021)
Баланс дебетуемой стороны < `Δ (WALLET_BALANCE_NEGATIVE_FORBIDDEN`, INV-WLT-3)нет
Счёт получателя не найден (WALLET_ACCOUNT_NOT_FOUND)нетДа + алертрассинхрон: кредит по получению денег не проведён — сверка с consumer-ом получения денег

Наблюдаемость и безопасность

  • Logs: consumed / skipped-duplicate / posted / failed — AccrualId, Version, id транзакции, без сумм и без текста Reason (Sensitive). Metrics: wallet_transactions_recorded_total{kind="Adjustment"}, дубликаты, DLQ, lag (now − время события).
  • Traces: span приёма → span проведения; корреляция из метаданных события.
  • PII нет; суммы и балансы — Sensitive-класс.

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

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

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