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

Entity / Aggregate: PayoutOrderEntity

Дом полей агрегата «Платёжное поручение» контекста rewards. Проекции (история выплат, сводки ЛК агентства, payload событий) ссылаются сюда и описывают только дельту.

Назначение

PayoutOrderEntity — сводное платёжное поручение на выплату вознаграждений (матрица «Агенство» 5.4: «генерация сводных отчётов для бухгалтерии для выплаты вознаграждений агентам»; 5.5: «история выплат — список всех выплат с датами, суммами, статусами: ожидается, выплачено, отклонено»). Поручения бывают двух видов (Kind):

  • AgencyCommission — поручение агентства на выплату долей агентам: собирает свободные Agent-строки распределения начислений в статусе MoneyReceived — деньги от застройщика получены, гейт ADR-0067 пройден (commission-accrual.md, CommissionDistributionEntity) — за период и группирует их по агентам в позиции (PayoutOrderItemEntity).
  • ReferralReward — поручение платформы (tenant — компания ПИН) на выплату реферальных вознаграждений партнёрам: собирает реферальные начисления в статусе Approved (referral-accrual.md), не включённые в другое поручение, и группирует их по партнёрам в позиции; MarkPaid каскадно переводит привязанные начисления Approved → Paid.

Исполнение платежей (интеграция с банком/эквайрингом) — реализация отложена (решение №9 ТЗ, по аналогии с crm): проектируется полная структура и статусная модель, а переход в Paid выполняется вручную — руководителем/бухгалтером агентства (AgencyCommission) или оператором ПИН (ReferralReward) после фактической выплаты вне платформы. История выплат = список поручений с их статусами и датами.

Классификация

ПолеЗначение
Контекстrewards
KindAggregateRoot (+ дочерняя Entity PayoutOrderItemEntity внутри границы агрегата)
Source of truthrewards
Таблицыrewards.payout_orders, rewards.payout_order_items
Tenant isolationTenantId = CompanyId агентства (AgencyCommission) / компании ПИН (ReferralReward) — владелец процесса выплаты (ADR-0052)

Поля (rewards.payout_orders)

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappУникальный идентификатор поручения.
TenantIdGuidДаindex ix_payout_orders_tenant_status (btree: tenant_id, status)identity (актор)Владелец поручения: агентство (AgencyCommission) / компания ПИН (ReferralReward).
KindPayoutOrderKindДахранится как text, varchar(32); index ix_payout_orders_tenant_kind (tenant_id, kind)appВид поручения (enum ниже): определяет источник строк (INV-1), tenant-владельца и каскад MarkPaid (INV-7). Задаётся при создании, неизменяем.
NumberstringДаvarchar(32), unique ux_payout_orders_tenant_number (tenant_id, number); формат PO-{yyyy}-{seq}, seq — PostgreSQL sequence per tenant-year (генерация в команде создания)appЧеловекочитаемый номер для бухгалтерии («ПП-2026-0012»).
Titlestring?Нетvarchar(256)appНазвание/назначение («Выплаты за июнь 2026»).
PeriodFromDateTimeOffsetДаcheck period_to > period_fromappНачало периода: включаются строки-источники по сделкам с DealCompletedAt >= PeriodFrom.
PeriodToDateTimeOffsetДаindex ix_payout_orders_period_toappКонец периода (исключительно): DealCompletedAt < PeriodTo.
StatusPayoutOrderStatusДахранится как text, varchar(32)appСтатусная модель ниже.
TotalAmountMoney (owned, VO — commission-accrual.md)Даtotal_amount_value numeric(18,2) >= 0, total_amount_currency_id smallint; check = Σ items (INV-3)вычислениеИтог поручения = сумма всех позиций.
ItemsCountintДа>= 0; денормализация для списковвычислениеЧисло позиций (получателей) в поручении.
ApprovedAtDateTimeOffset?Нетпри ApproveappМомент утверждения руководителем.
ApprovedByUserIdGuid?НетappУтвердивший.
PaidAtDateTimeOffset?Нетпри MarkPaidappМомент отметки о фактической выплате.
PaidByUserIdGuid?НетappКто отметил выплату.
RejectedAtDateTimeOffset?Нетпри RejectappМомент отклонения.
RejectReasonstring?Да при Rejectedvarchar(1000)appОбязательный комментарий отклонения (матрица 5.5 «отклонено»).
CancelledAtDateTimeOffset?Нетпри CancelappМомент отмены черновика.
CreatedAtDateTimeOffsetДаindex ix_payout_orders_created_atappМомент создания (аудит).
CreatedByUserIdGuid?НетappСоздавший (руководитель/бухгалтер агентства).
UpdatedAtDateTimeOffsetДаappМомент изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
VersionlongДа>= 1appДоменная версия (инкремент при мутации).

Enum PayoutOrderStatus

ЗначениеОписаниеМаппинг на статусы матрицы 5.5
DraftЧерновик: состав позиций редактируется (добавление/исключение распределений).
PendingPaymentУтверждено руководителем: состав заморожен, передано в бухгалтерию; ожидает фактической выплаты.«ожидается»
PaidВыплата произведена (отметка вручную — исполнение платежей отложено). Терминальный.«выплачено»
RejectedОтклонено (руководителем/бухгалтерией) с обязательной причиной; строки-источники освобождены. Терминальный.«отклонено»
CancelledЧерновик отменён создателем; строки-источники освобождены. Терминальный.

Enum PayoutOrderKind

ЗначениеОписание
AgencyCommissionВыплата долей агентов. Tenant — агентство; позиции — по агентам; источник строк — свободные Agent-строки распределения начислений в статусе MoneyReceived (commission-accrual.md). Управляет руководитель/бухгалтер агентства.
ReferralRewardВыплата реферальных вознаграждений. Tenant — компания ПИН; позиции — по партнёрам; источник строк — реферальные начисления в статусе Approved, не включённые в другое поручение (referral-accrual.md); MarkPaid каскадит Approved → Paid. Управляет оператор ПИН (платформенный scope, операции — область api/platform).

Дочерняя сущность PayoutOrderItemEntity (rewards.payout_order_items)

Позиция поручения — сводная выплата одному получателю: агенту (AgencyCommission) или реферальному партнёру (ReferralReward). Kind: Entity внутри границы агрегата PayoutOrderEntity; мутации только через корень.

ПолеТипОбязательностьОграниченияИсточникОписание
IdGuidДаPKappИдентификатор позиции.
PayoutOrderIdGuidДаFK fk_payout_order_items_order → payout_orders.id (cascade), indexappРодительское поручение.
TenantIdGuidДаindexродительДублируется для изоляции по tenant.
RecipientUserIdGuidДаunique ux_payout_order_items_order_recipient (payout_order_id, recipient_user_id) — один получатель = одна позиция (INV-2)строки-источникиПолучатель позиции: агент (AgencyCommission) / реферальный партнёр (ReferralReward).
AmountMoney (owned)Даcheck amount_value > 0; = Σ привязанных строк-источников (INV-3)вычислениеСумма выплаты получателю по позиции.
DistributionsCountintДа>= 1вычислениеЧисло строк-источников в позиции: Agent-строк распределения (AgencyCommission, обратная ссылка commission_distributions.payout_order_item_id, commission-accrual.md) / реферальных начислений (ReferralReward, обратная ссылка referral_accruals.payout_order_item_id, referral-accrual.md).
PaidAtDateTimeOffset?Нетпроставляется при MarkPaid порученияappМомент выплаты позиции.
CreatedAt / UpdatedAtDateTimeOffsetДаappАудит позиции.

Реквизиты получателя (счёт/карта) в rewards не хранятся — прохождение платежа вне платформы; при включении интеграции платежей реквизиты будут запрашиваться из profile (../../profile/domain/requisite-template.md — будущий путь по конвенции) в момент исполнения, не копируясь в поручение.

Value objects

Money — единый VO контекста, дом — commission-accrual.md (колонки total_amount_value/total_amount_currency_id, amount_value/amount_currency_id; CurrencyId short — ссылка на валюту справочника, RUB-only по ADR-0048). CurrencyId всех позиций равен CurrencyId поручения (INV-4).

Модели JSONB (versioned)

JSONB-модели не используются.

Связи

СвязьТипКлючDelete behavior
PayoutOrderItemEntityowned (внутри агрегата)PayoutOrderIdcascade
CommissionDistributionEntity (AgencyCommission)reference (обратная, из чужого агрегата CommissionAccrualEntity)commission_distributions.payout_order_item_id → payout_order_items.idset null (Reject/Cancel освобождает строки)
ReferralAccrualEntity (ReferralReward)reference (обратная, из чужого агрегата)referral_accruals.payout_order_item_id → payout_order_items.idset null (Reject/Cancel освобождает начисления)
CompanyEntity (агентство/компания ПИН, identity)reference (логическая, без FK)TenantIdrestrict

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-1 OnlyFreeSourceRowsИсточник строк — по Kind: AgencyCommission — только Agent-строки распределения начислений в статусе MoneyReceived (гейт получения денег, ADR-0067) с PayoutOrderItemId is null; ReferralReward — только реферальные начисления в статусе Approved с PayoutOrderItemId is null. Включение проставляет PayoutOrderItemId (строка занята ровно одним поручением)сборка состава: свободные строки захватываются под блокировкой и помечаются в одном согласованном измененииREWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE
INV-2 OneItemPerRecipientОдин получатель — одна позиция в порученииunique index + агрегация при сборкеREWARDS_PAYOUT_DUPLICATE_RECIPIENT
INV-3 SumConsistencyItem.Amount = Σ Amount привязанных строк-источников; TotalAmount = Σ Item.Amount; ItemsCount = count(Items)пересчёт в методах агрегата при каждой мутации составаREWARDS_PAYOUT_SUM_MISMATCH
INV-4 SingleCurrencyCurrencyId всех позиций и TotalAmount единметод сборки состава (VO-guard)REWARDS_CURRENCY_MISMATCH
INV-5 ComposeOnlyInDraftИзменение состава (добавить/исключить строки) — только в Draftguard по Status в методах агрегатаREWARDS_PAYOUT_COMPOSITION_LOCKED
INV-6 ApproveGuardApprove допустим только из Draft при ItemsCount >= 1; актор с permission rewards.payouts.approveметод агрегата + policyREWARDS_PAYOUT_INVALID_STATUS_TRANSITION
INV-7 MarkPaidCascadeMarkPaid (из PendingPayment): проставляет PaidAt поручению, позициям и привязанным строкам-источникам; каскад по Kind: AgencyCommission — каждое затронутое начисление, у которого все Agent-строки выплачены, переводится MoneyReceived → Paid; ReferralReward — каждое привязанное реферальное начисление переводится Approved → Paid — в одном согласованном изменениипереход MarkPaidREWARDS_PAYOUT_INVALID_STATUS_TRANSITION
INV-8 ReleaseOnTerminateReject/Cancel: у всех привязанных строк-источников PayoutOrderItemId → null (строки снова доступны следующему поручению); Reject требует RejectReasonпереходы Reject/CancelREWARDS_PAYOUT_REJECT_REASON_REQUIRED
INV-9 TenantOwnershipВсе строки-источники принадлежат TenantId поручения; для ReferralReward tenant — компания ПИН (реферальные начисления — платформенные)изоляция по tenant + проверка при сборкеfail closed
INV-10 KindHomogeneityВсе позиции и строки-источники поручения — одного вида, соответствующего Kind; смешение агентских долей и реферальных начислений в одном поручении запрещеносборка составаREWARDS_PAYOUT_KIND_MISMATCH

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
PayoutOrderCreatedEventдоменноеУспешный CreateId, TenantId, PeriodFrom/To
PayoutOrderApprovedEventдоменноеDraft → PendingPaymentId, TotalAmount, ItemsCount
PayoutOrderStatusChangedKafkaEventинтеграционное (ContractVersion = 1, topic pin.rewards.payout-order-status-changed.v1, key = Id, AggregateId = Id)Переходы Approve / MarkPaid / RejectId, TenantId, Kind, Number, FromStatus, ToStatus, TotalAmount (Value + CurrencyId + CurrencySnapshot../../handbook/domain/snapshots.md), ItemsCount, Items[] (PayoutOrderItemId, RecipientUserId, Amount, PaidAt? — при Paid строки дебета wallet), PaidAt?, RejectReason?, Version. Контракт — ../events/payout-order-status-changed.md. Потребители: notifications (уведомления получателям и владельцу поручения), wallet (дебет счетов при Paid, идемпотентность по PayoutOrderItemId)

Интеграционные события публикуются атомарно со сменой состояния агрегата.

Дебет кошелька. По событию перехода в Paid контекст wallet списывает суммы позиций (Items[]) со счетов получателей (../../wallet/README.md): доля агента — со счёта AgentCommission, реферальное вознаграждение — со счёта ReferralCommission (ADR-0065). Сам rewards балансов не хранит и кошельком не управляет; кредит счетов происходит раньше — по событию получения денег (commission-accrual.md, commission-money-received).

Конкурентность и восстановление

  • Гонка за свободные строки-источники. Два поручения одного tenant-а, собираемых за пересекающийся период, могут претендовать на одну свободную строку (Agent-строку распределения или реферальное начисление). Строки захватываются под блокировкой и помечаются PayoutOrderItemId в одном согласованном изменении (INV-1) — победитель занимает строку, проигравший получает REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE и пересобирает состав без уже занятых строк.
  • Гонка сборки vs отмена/корректировка начисления. Начисление в MoneyReceived не отменяется вовсе (commission-accrual.md, INV-8) — конфликт с отменой исключён; корректировка распределения (INV-11 начисления) допустима только пока строка Agent не включена в поручение — обе операции мутируют агрегат начисления, оптимистическая блокировка пропускает одну. Реферальное начисление, включённое в поручение, не отменяется — освобождение только через Reject/Cancel поручения (referral-accrual.md).
  • Атомарность MarkPaid. Каскад INV-7 (поручение → позиции → строки-источники → начисления MoneyReceived → Paid / рефералки Approved → Paid) выполняется в одном согласованном изменении: частичной выплаты не бывает — либо всё поручение помечено Paid и все затронутые начисления перешли в Paid, либо ничего.
  • Уникальность номера. Number формируется из per-tenant-year sequence; конкурентные создания не дают коллизий, при гонке уникальный индекс ux_payout_orders_tenant_number страхует.
  • Освобождение при терминации. Reject/Cancel возвращают строкам-источникам PayoutOrderItemId = null (INV-8) — освобождённые строки снова доступны следующему поручению; связанные начисления остаются в MoneyReceived (рефералки — в Approved).
  • Оптимистическая блокировка переходов. Параллельные Approve/Cancel/Reject/MarkPaid над одним поручением разрешаются по Version: проходит один, проигравший перечитывает актуальный статус, его повтор отклоняется guard-ом статуса (REWARDS_PAYOUT_INVALID_STATUS_TRANSITION).

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK; ux_payout_orders_tenant_number unique btree (tenant_id, number); ix_payout_orders_tenant_status btree (tenant_id, status); ix_payout_orders_tenant_kind btree (tenant_id, kind); ix_payout_orders_period_to btree; ix_payout_orders_created_at btree; items: PK, ux_payout_order_items_order_recipient unique btree (payout_order_id, recipient_user_id), ix_payout_order_items_recipient btree (tenant_id, recipient_user_id) — история выплат получателя
Партиционирование / retentionnone — единицы поручений на tenant в месяц; retention бессрочный (финансовая история)
Concurrencyоптимистическая блокировка на переходах; захват строк-источников под блокировкой при сборке состава (INV-1)
Permissions / PIIСоздание/состав — rewards.payouts.manage; утверждение/отметка выплаты/отклонение — rewards.payouts.approve (для AgencyCommission — роль «Руководитель агентства»/бухгалтер, scope = организация; для ReferralReward — оператор ПИН, платформенный scope, операции — область api/platform); агент видит только свои позиции (rewards.payouts.view-own, record-level RecipientUserId), реферальный партнёр — свои начисления и выплаты по rewards.referral.view-own (record-level). Суммы — Sensitive: не логируются, в списках агентства видны только ролям с rewards.payouts.manage. Банковские реквизиты не хранятся (см. примечание к items)
AuditVersion + actor-stamping (ApprovedBy/PaidBy) + PayoutOrderStatusChangedKafkaEvent; история выплат = сами поручения (Paid/Rejected) — отдельная сущность истории не нужна

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

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

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

  • Контракт производимого события: ../events/payout-order-status-changed.md.
  • commission-accrual.md — источник Agent-строк (MoneyReceived), каскад Paid (INV-7).
  • referral-accrual.md — источник строк вида ReferralReward (Approved), каскад Paid.
  • agency-commission-policy.md — как формируются суммы долей (деление пула агентства).
  • Кошелёк (дебет по Paid, кредит по MoneyReceived): ../../wallet/README.md.
  • Реквизиты получателя (будущая интеграция платежей): ../../profile/domain/requisite-template.md (по конвенции).
  • Решение №9 ТЗ (technical-assignment.md) — «структуры проектируются; интеграция платежей — отложена».
  • ADR-0052 (tenancy), ADR-0056 (simplicity-first — ручная отметка вместо платёжного шлюза), ADR-0065 (кошелёк), ADR-0067 (гейт получения денег — источники строк).
  • Шаблон: entity-template.md; правила: cross-cutting-document-requirements.md.