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

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 typepin.rewards.payout-order-status-changed
C# typePayoutOrderStatusChangedKafkaEvent
Topicpin.rewards.payout-order-status-changed.v1
Partitions / key3 / key = Id поручения — строгий порядок переходов per-order (объём — единицы поручений на tenant в месяц, ADR-0056)
ContractVersion1
Producerrewards: утверждение / отметка выплаты / отклонение поручения. 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Описание
IdGuidДаPayoutOrderEntity.IdImmutableКлюч агрегата (= Kafka key).
TenantIdGuidДаPayoutOrderEntity.TenantIdImmutableВладелец поручения: агентство (AgencyCommission) / компания ПИН (ReferralReward) — tenant boundary.
Kindstring (enum PayoutOrderKind)ДаPayoutOrderEntity.KindAdditive only (новые значения)AgencyCommission / ReferralReward — определяет дебетуемые счета wallet и адресатов уведомлений.
NumberstringДаPayoutOrderEntity.NumberImmutableЧеловекочитаемый номер («PO-2026-0012») — текст уведомления.
FromStatusstring (enum PayoutOrderStatus)Дастатус до переходаAdditive only (новые значения)Draft / PendingPayment.
ToStatusstring (enum PayoutOrderStatus)ДаPayoutOrderEntity.Status после переходаAdditive onlyPendingPayment / Paid / Rejected.
TotalAmountMoneyModelДаPayoutOrderEntity.TotalAmountAdditive onlyИтог поручения (уведомление руководителю).
ItemsCountintДаPayoutOrderEntity.ItemsCountAdditive onlyЧисло позиций (агентов).
PaidAtDateTimeOffset?Да при ToStatus = Paid, иначе nullPayoutOrderEntity.PaidAtAdditive onlyМомент отметки о фактической выплате.
RejectReasonstring?Да при ToStatus = Rejected, иначе nullPayoutOrderEntity.RejectReasonAdditive onlyПричина отклонения (≤ 1000 символов) — показывается владельцу поручения; получателям не рассылается.
ItemsIReadOnlyList<PayoutOrderStatusItemModel>Да (≥ 1 элемент)PayoutOrderEntity.Items (PayoutOrderItemEntity)Additive onlyПозиции по получателям — адресаты уведомлений; при ToStatus = Paid — строки дебета wallet. Bounded: получателей в поручении ≤ числа агентов агентства (≤ 1000, лимит AgentOverrides политики) / партнёров периода.
VersionlongДаPayoutOrderEntity.Version после переходаImmutable semanticsДоменная версия — монотонный guard потребителя.

Вложенная модель PayoutOrderStatusItemModel

Проекция PayoutOrderItemEntity (payout-order.md); состав позиций заморожен с Approve (INV-5), поэтому список идентичен во всех событиях одного поручения после Draft.

ПолеТипОбязательностьОграниченияОписание
PayoutOrderItemIdGuidДаPayoutOrderItemEntity.IdИдентификатор позиции — ключ идемпотентности дебета wallet (повтор события не удваивает списание).
RecipientUserIdGuidДауникален в списке (INV-2 payout-order.md)Получатель позиции: агент (AgencyCommission) / реферальный партнёр (ReferralReward) — адресат уведомления и держатель дебетуемого счёта.
AmountMoneyModelДаValue > 0Сумма выплаты получателю по позиции.
PaidAtDateTimeOffset?Да при ToStatus = Paid, иначе nullМомент выплаты позиции.

Вложенная модель MoneyModel

Проекция VO Money (commission-accrual.md, раздел «Value objects»); валюта едина для всех позиций и итога (INV-4).

ПолеТипОбязательностьОграниченияОписание
ValuedecimalДа>= 0, scale 2Сумма.
CurrencyIdshortДассылка на валюту справочника; RUB-only (ADR-0048)Типизированная ссылка на валюту.
CurrencySnapshotCurrencySnapshotДа../../handbook/domain/snapshots.mdDisplay-снапшот валюты на момент публикации.

Когда публикуется

По 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.approveDraft → 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 — только владельцу с RejectReasonMessageId; порядок — 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): переходы одного поручения (ApproveMarkPaid/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); unknown Kind у wallet — сообщение в DLQ + алерт (списание по неизвестному виду поручения не выполняется молча), у notifications — нейтральный текст уведомления.
  • Deprecation — по общему sunset-правилу событийного реестра.

Безопасность

ПолеКласс данныхПравило
Id, TenantId, Kind, Number, FromStatus, ToStatus, ItemsCount, PaidAt, VersionInternalinclude
TotalAmount, Items[].AmountSensitive (суммы выплат, README «Безопасность»)include (внутренний топик, m2m ACL Kafka — notifications/wallet); в логи не пишутся; получатель в уведомлении видит только свою позицию, владелец поручения — итог
Items[].PayoutOrderItemId, Items[].RecipientUserIdInternalinclude (идемпотентность дебетов, адресация уведомлений)
RejectReasonInternalinclude; рассылается только ролям с rewards.payouts.manage, получателям не показывается

Событие НЕ содержит банковские реквизиты (в rewards не хранятся — payout-order.md), ссылки на начисления/сделки, PII, secrets, unbounded-вложения (Items bounded).

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

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

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