Event Contract: pin.rewards.payout-order-status-changed (PayoutOrderStatusChangedKafkaEvent)
Версионируемый контракт интеграционного события «статус платёжного поручения изменился».
Consumer-документы ссылаются сюда, не переописывая payload.
Дом полей — entity: PayoutOrderEntity.
Назначение
Фиксирует движение платёжного поручения по статусной модели «история выплат»: утверждение
(Draft → PendingPayment — поручение сформировано и передано в бухгалтерию, «ожидается»), выплату
(PendingPayment → Paid — ручная отметка, исполнение платежей отложено, решение №9 ТЗ) и отклонение
(PendingPayment → Rejected с причиной). Покрывает оба вида поручений (Kind,
payout-order.md): AgencyCommission — выплаты долей агентам
(владелец — агентство) и ReferralReward — выплаты реферальных вознаграждений партнёрам (владелец —
компания ПИН). Producer — rewards; потребители: notifications — уведомления получателям позиций и
владельцу поручения без синхронного запроса в rewards; wallet — дебет счетов получателей при
Paid (ADR-0065). История выплат в ЛК события не потребляет — это список самих поручений из хранилища
rewards (решение №6 ../README.md, ADR-0056).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.rewards.payout-order-status-changed |
| C# type | PayoutOrderStatusChangedKafkaEvent |
| Topic | pin.rewards.payout-order-status-changed.v1 |
| Partitions / key | 3 / key = Id поручения — строгий порядок переходов per-order (объём — единицы поручений на tenant в месяц, ADR-0056) |
| ContractVersion | 1 |
| Producer | rewards: утверждение / отметка выплаты / отклонение поручения. AgencyCommission — agency-API: ../api/agency/approve-payout-order.md, ../api/agency/mark-payout-order-paid.md, ../api/agency/reject-payout-order.md; ReferralReward — platform-API: api/platform/approve-referral-payout-order.md, api/platform/mark-referral-payout-order-paid.md (по конвенции, карта областей ../README.md) |
| Статус | draft |
Публикуется атомарно с переходом статуса поручения (для отметки о выплате — атомарно со всем каскадом
INV-7: позиции, строки-источники и начисления — MoneyReceived → Paid для AgencyCommission,
рефералки Approved → Paid для ReferralReward — фиксируются в одном согласованном изменении). Ключ
Kafka = Id, порядок событий — per-order; дедупликация у потребителя — по идентификатору сообщения,
монотонность — по Version.
Payload PayoutOrderStatusChangedKafkaEvent
Snapshot-поля — из дома payout-order.md. Коллекция Items —
контракт-уровневое дополнение к сводке событий в entity-документе: адресные уведомления получателям и
дебеты wallet без lookup в rewards; дом полей позиций — тот же entity-документ
(PayoutOrderItemEntity).
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PayoutOrderEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
TenantId | Guid | Да | PayoutOrderEntity.TenantId | Immutable | Владелец поручения: агентство (AgencyCommission) / компания ПИН (ReferralReward) — tenant boundary. |
Kind | string (enum PayoutOrderKind) | Да | PayoutOrderEntity.Kind | Additive only (новые значения) | AgencyCommission / ReferralReward — определяет дебетуемые счета wallet и адресатов уведомлений. |
Number | string | Да | PayoutOrderEntity.Number | Immutable | Человекочитаемый номер («PO-2026-0012») — текст уведомления. |
FromStatus | string (enum PayoutOrderStatus) | Да | статус до перехода | Additive only (новые значения) | Draft / PendingPayment. |
ToStatus | string (enum PayoutOrderStatus) | Да | PayoutOrderEntity.Status после перехода | Additive only | PendingPayment / Paid / Rejected. |
TotalAmount | MoneyModel | Да | PayoutOrderEntity.TotalAmount | Additive only | Итог поручения (уведомление руководителю). |
ItemsCount | int | Да | PayoutOrderEntity.ItemsCount | Additive only | Число позиций (агентов). |
PaidAt | DateTimeOffset? | Да при ToStatus = Paid, иначе null | PayoutOrderEntity.PaidAt | Additive only | Момент отметки о фактической выплате. |
RejectReason | string? | Да при ToStatus = Rejected, иначе null | PayoutOrderEntity.RejectReason | Additive only | Причина отклонения (≤ 1000 символов) — показывается владельцу поручения; получателям не рассылается. |
Items | IReadOnlyList<PayoutOrderStatusItemModel> | Да (≥ 1 элемент) | PayoutOrderEntity.Items (PayoutOrderItemEntity) | Additive only | Позиции по получателям — адресаты уведомлений; при ToStatus = Paid — строки дебета wallet. Bounded: получателей в поручении ≤ числа агентов агентства (≤ 1000, лимит AgentOverrides политики) / партнёров периода. |
Version | long | Да | PayoutOrderEntity.Version после перехода | Immutable semantics | Доменная версия — монотонный guard потребителя. |
Вложенная модель PayoutOrderStatusItemModel
Проекция PayoutOrderItemEntity (payout-order.md); состав позиций
заморожен с Approve (INV-5), поэтому список идентичен во всех событиях одного поручения после
Draft.
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
PayoutOrderItemId | Guid | Да | PayoutOrderItemEntity.Id | Идентификатор позиции — ключ идемпотентности дебета wallet (повтор события не удваивает списание). |
RecipientUserId | Guid | Да | уникален в списке (INV-2 payout-order.md) | Получатель позиции: агент (AgencyCommission) / реферальный партнёр (ReferralReward) — адресат уведомления и держатель дебетуемого счёта. |
Amount | MoneyModel | Да | Value > 0 | Сумма выплаты получателю по позиции. |
PaidAt | DateTimeOffset? | Да при ToStatus = Paid, иначе null | — | Момент выплаты позиции. |
Вложенная модель MoneyModel
Проекция VO Money (commission-accrual.md, раздел «Value objects»);
валюта едина для всех позиций и итога (INV-4).
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
Value | decimal | Да | >= 0, scale 2 | Сумма. |
CurrencyId | short | Да | ссылка на валюту справочника; RUB-only (ADR-0048) | Типизированная ссылка на валюту. |
CurrencySnapshot | CurrencySnapshot | Да | ../../handbook/domain/snapshots.md | Display-снапшот валюты на момент публикации. |
Когда публикуется
По stateDiagram payout-order.md — только переходы утверждения, отметки выплаты и отклонения:
| Условие | Переход состояния | Согласованность |
|---|---|---|
Владелец поручения утвердил его: руководитель агентства (../api/agency/approve-payout-order.md, AgencyCommission) / оператор ПИН (api/platform/approve-referral-payout-order.md — по конвенции, ReferralReward); guard INV-6: ItemsCount >= 1, permission rewards.payouts.approve | Draft → PendingPayment | Поручение и факт публикации — в одном согласованном изменении |
Отметка фактической выплаты: руководитель/бухгалтер агентства (../api/agency/mark-payout-order-paid.md) / оператор ПИН (api/platform/mark-referral-payout-order-paid.md — по конвенции); исполнение платежей вне платформы, решение №9 ТЗ | PendingPayment → Paid | Каскад INV-7: поручение, PaidAt позиций и строк-источников, переходы начислений MoneyReceived → Paid / рефералок Approved → Paid (порождают собственные commission-accrual-status-changed / referral-accrual-status-changed) и факт публикации — в одном согласованном изменении |
| Отклонение с обязательной причиной (../api/agency/reject-payout-order.md; INV-8: строки-источники освобождаются) | PendingPayment → Rejected | Поручение, освобождение строк-источников (PayoutOrderItemId → null) и факт публикации — в одном согласованном изменении |
Создание черновика и отмена черновика события не публикуют — черновик не виден за пределами
владельца поручения, уведомлять некого (доменные PayoutOrderCreatedEvent — внутренние).
Потребители (consumers)
| Потребитель | Эффект (событие → действие) | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
notifications | По ToStatus: PendingPayment — каждому Items[].RecipientUserId «вам направлена выплата {Amount}» + владельцу поручения (TenantId, роль с rewards.payouts.manage) «поручение {Number} утверждено»; Paid — каждому получателю «выплата произведена» + владельцу; Rejected — только владельцу с RejectReason | MessageId; порядок — key = Id + Version-guard | < 5 мин |
wallet | Только при ToStatus = Paid (ADR-0065): дебет счёта каждого получателя Items[] на Amount — по Kind: AgencyCommission → счёт AgentCommission агента, ReferralReward → счёт ReferralCommission партнёра. Недостаток баланса (рассинхрон) — сообщение в DLQ + алерт (ADR-0021). Остальные переходы — no-op. Consumer — ../../wallet/consumers/rewards-payout-order-status-changed.md (по конвенции) | MessageId + бизнес-ключ операции (вид операции + Items[].PayoutOrderItemId) — повтор не удваивает списания | < 5 мин |
Гарантии доставки и порядка
- Доставка — at-least-once;
notificationsдедуплицирует поMessageId(уведомление не рассылается дважды),wallet— поMessageIdи бизнес-ключу операции per-позиция (повторная доставка не удваивает списания). - Порядок внутри топика — строгий per-order (key =
Id): переходы одного поручения (Approve→MarkPaid/Reject) приходят в причинном порядке; monotonic-guard — поVersion. - Каскад выплаты (несколько топиков).
MarkPaidатомарно порождает и это событие, и по одному событию статуса на каждое затронутое начисление:commission-accrual-status-changed.v1(MoneyReceived → Paid,AgencyCommission) /pin.rewards.referral-accruals.v1(Approved → Paid,ReferralReward); сквозного порядка между топиками нет, но получатели уведомлений разные (получателю — «выплата произведена» из этого топика; статус начисления — из соседних), поэтому дублирующих уведомлений не возникает; дебетwalletвыполняется только по этому топику (кредиты — поcommission-money-receivedи реферальному топику), двойных проводок нет. - Дебет строго после кредита по бизнес-порядку. Позиции поручения собираются только из
начислений/рефералок, прошедших гейт денег (INV-1 payout-order.md), — кредит кошелька причинно
предшествует дебету; при межтопиковом опережении дебета
walletуводит сообщение в retry/DLQ (баланс ещё не кредитован), итог сходится. - Состав позиций стабилен после
Approve(INV-5 payout-order.md):Itemsидентичен во всех событиях одного поручения послеDraft, что делает адресацию уведомлений и дебетов детерминированной.
Версионирование и совместимость
ContractVersion = 1; несовпадение версии контракта в сообщении — warn-and-proceed.- Additive-only без bump (новые optional-поля payload/
Items, новые значения enum). - Breaking —
ContractVersion = 2+ топикpin.rewards.payout-order-status-changed.v2+ dual-publish + deadline миграции consumers + sunset. - Unknown enum
FromStatus/ToStatusу потребителя — уведомление не отправляется, warning + метрика (fail-safe); unknownKindуwallet— сообщение в DLQ + алерт (списание по неизвестному виду поручения не выполняется молча), уnotifications— нейтральный текст уведомления. - Deprecation — по общему sunset-правилу событийного реестра.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
Id, TenantId, Kind, Number, FromStatus, ToStatus, ItemsCount, PaidAt, Version | Internal | include |
TotalAmount, Items[].Amount | Sensitive (суммы выплат, README «Безопасность») | include (внутренний топик, m2m ACL Kafka — notifications/wallet); в логи не пишутся; получатель в уведомлении видит только свою позицию, владелец поручения — итог |
Items[].PayoutOrderItemId, Items[].RecipientUserId | Internal | include (идемпотентность дебетов, адресация уведомлений) |
RejectReason | Internal | include; рассылается только ролям с rewards.payouts.manage, получателям не показывается |
Событие НЕ содержит банковские реквизиты (в rewards не хранятся — payout-order.md),
ссылки на начисления/сделки, PII, secrets, unbounded-вложения (Items bounded).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: PayoutOrderEntity
- API (6): POST /api/v1/agency/payout-orders — Создание платёжного поручения, POST /api/v1/agency/payout-orders/{orderId}/approve — Утверждение платёжного поручения, POST /api/v1/agency/payout-orders/{orderId}/mark-paid — Ручная отметка о выплате, POST /api/v1/agency/payout-orders/{orderId}/reject — Отклонение поручения, POST /api/v1/platform/referral-payout-orders/{orderId}/approve — Утверждение реферального поручения, POST /api/v1/platform/referral-payout-orders/{orderId}/mark-paid — Отметка о выплате реферального поручения
- Use Cases (4): UC-RWD-009. Жизненный цикл реферального начисления (связь → начисление → гейт → выплата), UC-WLT-002. Выплата поручения дебетует баланс получателя, US-реестр: Владелец агентства (US-OWN-AG), US-реестр: Финансовый блок ПИН (US-FIN)
- События (4): Event Contract: pin.rewards.commission-accrual-status-changed (CommissionAccrualStatusChangedKafkaEvent), Event Contract: pin.rewards.commission-money-received (CommissionMoneyReceivedKafkaEvent), Event Contract: pin.rewards.referral-accrual-status-changed (ReferralAccrualStatusChangedKafkaEvent), Event Contract: pin.wallet.transaction-recorded (WalletTransactionRecordedKafkaEvent)
- Консюмеры (1): Consumer: Rewards Payout Order Status Changed
- Саги (1): Saga «Commission Settlement» (финансовый контур вознаграждений)
- registries (1): Реестр событий PIN
- index.md (2): Rewards Service, Wallet Service
Связанные документы
- Сущность: payout-order.md (stateDiagram,
PayoutOrderKind, INV-1/5..8/10). - Каскадные события выплаты:
commission-accrual-status-changed.md
(
MoneyReceived → Paid), referral-accrual-status-changed.md (Approved → Paid). - Источники строк: commission-accrual.md
(
CommissionDistributionEntity,Agent-строки вMoneyReceived), referral-accrual.md (Approved-рефералки). - Кредит счетов (предшествует дебету): commission-money-received.md;
кошелёк —
../../wallet/consumers/rewards-payout-order-status-changed.md(по конвенции), ADR-0065. - Snapshot-модели валюты: ../../handbook/domain/snapshots.md.