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

Entity / Aggregate: PlatformReceiptEntity

Дом полей агрегата «Акт поступления средств» контекста rewards (+ дочерняя сущность PlatformReceiptItemEntity). Проекции (реестр актов platform-API, payload событий получения денег) ссылаются сюда и описывают только дельту.

Назначение

PlatformReceiptEntity — «Акт поступления средств»: документ, которым финансовый оператор ПИН подтверждает фактическое получение комиссионных денег от застройщика (ADR-0067). Застройщик перечисляет комиссию пакетно — одним платежом за несколько сделок; акт связывает пришедшую сумму с конкретным набором подтверждённых начислений (commission-accrual.md) и контролирует их сходимость (Σ позиций = сумме акта).

Регистрация акта — единый гейт всех денежных следствий: позиции атомарно переходят Confirmed → MoneyReceived, связанные реферальные начисления (referral-accrual.md) — Pending → Approved, публикуются события получения денег (кредиты кошелька, ADR-0065). До регистрации акта никакие выплаты (агентские и реферальные) не допускаются. Частичные оплаты — вне scope v1: если поступило меньше, акт формируется только на полностью покрытые начисления; частичное покрытие одного начисления не моделируется.

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

ПолеЗначение
Контекстrewards
KindAggregateRoot (+ дочерняя Entity PlatformReceiptItemEntity внутри границы агрегата)
Source of truthrewards
Таблицыrewards.platform_receipts, rewards.platform_receipt_items
Tenant isolationвне tenant-модели — платформенная сущность (владелец процесса — ПИН, ADR-0052); позиции могут ссылаться на начисления разных агентств одного застройщика

Поля (rewards.platform_receipts)

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappУникальный идентификатор акта.
NumberstringДаvarchar(32), unique ux_platform_receipts_number; формат RC-{yyyy}-{seq}, seq — платформенный PostgreSQL sequence per year (генерация при создании)appЧеловекочитаемый номер акта («RC-2026-0042») — сверка с бухгалтерией.
DeveloperCompanyIdGuidДаindex ix_platform_receipts_developer (developer_company_id, status); логическая ссылка на identity.companies.id (Type = Developer), без FKappЗастройщик-плательщик — акт формируется по одному застройщику (INV-RC-1).
AmountMoney (owned)Дасм. VO; check amount_value > 0appСумма фактического поступления (из платёжного документа застройщика); при регистрации обязана сойтись с Σ позиций (INV-RC-2).
ItemsCountintДа>= 0; денормализация для списковвычислениеЧисло позиций акта.
PaymentReferencestring?Нетvarchar(256)appРеквизит платёжного документа застройщика (номер/дата платёжки) — след для сверки.
Commentstring?Нетvarchar(2000)appКомментарий оператора ПИН.
StatusPlatformReceiptStatusДахранится как text, varchar(32), index (status)appDraft / Registered / Cancelled (stateDiagram ниже).
RegisteredAtDateTimeOffset?Нетзаполняется при RegisterappМомент регистрации (= MoneyReceivedAt позиций).
RegisteredByUserIdGuid?НетappФинансовый оператор ПИН, зарегистрировавший акт.
CancelledAtDateTimeOffset?Нетзаполняется при CancelappМомент отмены черновика.
CancelledByUserIdGuid?НетappКто отменил.
CreatedAtDateTimeOffsetДаindexappМомент создания (аудит).
CreatedByUserIdGuid?НетappСоздавший оператор.
UpdatedAtDateTimeOffsetДаappМомент изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
VersionlongДа>= 1appДоменная версия (инкремент при мутации).

Enum PlatformReceiptStatus

ЗначениеОписание
DraftЧерновик: состав позиций редактируется; предпросмотр Σ позиций против Amount.
RegisteredЗарегистрирован — финансовый документ: неизменяем, не откатывается (INV-RC-4); позиции переведены в MoneyReceived, рефералки апрувнуты, события опубликованы. Терминальный.
CancelledЧерновик отменён: позиции освобождены — начисления доступны другому акту. Терминальный.

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

Позиция акта — одно подтверждённое комиссионное начисление, покрытое поступлением. Kind: Entity внутри границы агрегата PlatformReceiptEntity; мутации только через корень. Позиции отменённого акта физически удаляются при Cancel (черновиковый состав — рабочие данные, не финансовый след), поэтому уникальный индекс по начислению структурно гарантирует эксклюзивность (INV-RC-3).

ПолеТипОбязательностьОграниченияИсточникОписание
IdGuidДаPKappИдентификатор позиции.
ReceiptIdGuidДаFK fk_platform_receipt_items_receipt → platform_receipts.id (cascade), indexappРодительский акт.
CommissionAccrualIdGuidДаFK fk_platform_receipt_items_accrual → rewards.commission_accruals.id (restrict); unique ux_platform_receipt_items_accrual (commission_accrual_id) — начисление максимум в одном не-отменённом акте (INV-RC-3)appПокрываемое начисление (commission-accrual.md); на момент включения — Confirmed того же застройщика (INV-RC-1).
AgencyCompanyIdGuidДаindexначисление (TenantId)Агентство начисления — денормализация для отображения состава (позиции разных агентств одного застройщика).
AmountMoney (owned)Даcheck amount_value > 0начисление (CommissionAmount)Сумма позиции — CommissionAmount начисления на момент включения; при регистрации сверяется с актуальной (коды ошибок ниже).
CreatedAtDateTimeOffsetДаappМомент включения позиции.

Value objects

Money (единый VO контекста rewards, дом — commission-accrual.md)

ПолеТипОбязательностьОграниченияКолонка (пример для Amount акта)
ValuedecimalДаnumeric(18,2), >= 0amount_value
CurrencyIdshortДассылка на валюту справочника (../../handbook/domain/snapshots.md); RUB-only (ADR-0048)amount_currency_id

Валюта акта и всех позиций едина и совпадает с валютой покрываемых начислений (REWARDS_CURRENCY_MISMATCH).

Модели JSONB

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

Связи

СвязьТипКлючDelete behavior
PlatformReceiptItemEntityowned (внутри агрегата)ReceiptIdcascade (позиции Cancel удаляются — начисления освобождаются)
CommissionAccrualEntityreference (со стороны позиции)CommissionAccrualId; обратная ссылка commission_accruals.platform_receipt_id заполняется при Registerrestrict
ReferralAccrualEntityкосвенная (через родительское начисление)referral_accruals.commission_accrual_id— (каскад авто-апрува при Register)
CompanyEntity (застройщик, identity)reference (логическая, без FK)DeveloperCompanyIdrestrict

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

ИнвариантУсловиеГде проверяетсяОшибка
INV-RC-1 OnlyConfirmedSameDeveloperПозиция ссылается на начисление в статусе Confirmed с DeveloperCompanyId = застройщику актавключение позиции + повторная проверка при RegisterREWARDS_RECEIPT_ITEM_INVALID
INV-RC-2 SumConsistencyΣ Item.Amount.Value = Amount.Value акта — сходимость поступления с покрытием; ItemsCount = count(Items), ItemsCount >= 1 при регистрацииоперация Register (пересчёт по актуальным суммам начислений)REWARDS_RECEIPT_SUM_MISMATCH
INV-RC-3 ExclusiveMembershipНачисление состоит максимум в одном не-отменённом акте (Draft или Registered) — двойное подтверждение получения исключено структурно (unique index; Cancel удаляет позиции)unique ux_platform_receipt_items_accrual при включении позицииREWARDS_RECEIPT_ACCRUAL_ALREADY_INCLUDED (гонка — конфликт индекса)
INV-RC-4 RegisteredImmutableAtomicCascadeRegistered — терминальный и неизменяемый (состав/поля не правятся, отката нет — ошибки лечатся компенсирующими начислениями/корректировками следующим периодом); регистрация выполняется атомарно: все позиции Confirmed → MoneyReceived (+ MoneyReceivedAt, PlatformReceiptId начисления), связанные рефералки в PendingApproved, публикация событий получения денег — либо всё, либо ничего. Изменение состава и полей — только в Draftоперация Register (одно согласованное изменение) + guard по StatusREWARDS_RECEIPT_REGISTERED_IMMUTABLE / REWARDS_RECEIPT_COMPOSITION_LOCKED

Коды ошибок

КодHTTPУсловие
REWARDS_RECEIPT_NOT_FOUND404Акт не найден (операции platform-API).
REWARDS_RECEIPT_ITEM_INVALID409Позиция ссылается на начисление не в Confirmed / чужого застройщика (INV-RC-1).
REWARDS_RECEIPT_ACCRUAL_ALREADY_INCLUDED409Начисление уже в составе другого не-отменённого акта (INV-RC-3).
REWARDS_RECEIPT_SUM_MISMATCH409Σ позиций ≠ сумме акта при регистрации (INV-RC-2).
REWARDS_RECEIPT_ITEM_STALE409Сумма позиции разошлась с актуальным CommissionAmount начисления (gross скорректирован после включения) — оператор пересобирает состав.
REWARDS_RECEIPT_REGISTERED_IMMUTABLE409Попытка изменить/отменить Registered-акт (INV-RC-4).
REWARDS_RECEIPT_COMPOSITION_LOCKED409Изменение состава вне Draft (INV-RC-4).
REWARDS_RECEIPT_INVALID_STATUS_TRANSITION409Недопустимый переход статуса.
REWARDS_CURRENCY_MISMATCH400Валюта позиции/акта не совпадает.

Регистрация (атомарный каскад)

  1. Перепроверить состав по актуальному состоянию начислений: каждое — Confirmed, того же застройщика (INV-RC-1), сумма позиции равна текущему CommissionAmount (расхождение — gross скорректирован после включения → REWARDS_RECEIPT_ITEM_STALE, оператор пересобирает состав); ItemsCount >= 1.
  2. Сверить сходимость: Σ позиций = Amount акта, иначе REWARDS_RECEIPT_SUM_MISMATCH (расхождение фактического платежа лечится до регистрации: корректировкой gross начисления или исключением непокрытых позиций — они остаются в Confirmed до следующего акта).
  3. В одном согласованном изменении: акт → Registered (RegisteredAt, актор); каждая позиция — начисление Confirmed → MoneyReceived (+ MoneyReceivedAt = RegisteredAt, PlatformReceiptId = Id акта); реферальные начисления в Pending, чьё родительское начисление вошло в акт, — Pending → Approved (referral-accrual.md, INV-RA-3).
  4. Атомарно с изменением публикуются события: pin.rewards.commission-money-received.v1 — по одному на каждое начисление акта (key = id начисления, payload включает строки распределения Platform/Agency/Agent); commission-accrual-status-changed (→ MoneyReceived) — по каждому начислению; referral-accruals status-changed (→ Approved) — по каждой апрувнутой рефералке.
  5. После регистрации: Agent-строки распределений доступны платёжным поручениям агентств, Approved-рефералки — реферальным поручениям (payout-order.md); кошелёк кредитует счета получателей по событиям (ADR-0065).

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

СобытиеТипКогдаSnapshot/поля
PlatformReceiptCreatedEventдоменноеУспешный CreateId, Number, DeveloperCompanyId, Amount
PlatformReceiptRegisteredEventдоменноеDraft → RegisteredId, Number, состав позиций
CommissionMoneyReceivedKafkaEventинтеграционное (ContractVersion = 1, topic pin.rewards.commission-money-received.v1, key = id начисления)Register — по каждому начислению актаid начисления, TenantId, DealId, DeveloperCompanyId, PlatformReceiptId, MoneyReceivedAt, CommissionAmount, Distributions[] (RecipientType Platform/Agency/Agent, RecipientUserId?, Amount: Value + CurrencyId + CurrencySnapshot), Version. Контракт — ../events/commission-money-received.md. Потребители: wallet (кредиты счетов по строкам), notifications («деньги получены» — агенту/агентству)

Смена статусов начислений и рефералок дополнительно публикует их собственные события (../events/commission-accrual-status-changed.md, топик pin.rewards.referral-accruals.v1referral-accrual.md). Все публикации атомарны с регистрацией.

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

  • Гонка включения начисления в два акта. Разрешается unique ux_platform_receipt_items_accrual (INV-RC-3): победитель включает позицию, проигравший получает REWARDS_RECEIPT_ACCRUAL_ALREADY_INCLUDED и пересобирает состав.
  • Гонка регистрации и корректировки gross. Корректировка gross ПИН-админом допустима в Confirmed (ADR-0067 п.4) — в т.ч. пока начисление в Draft-акте; перепроверка сумм на шаге 1 регистрации ловит расхождение (REWARDS_RECEIPT_ITEM_STALE); обе операции мутируют начисление под оптимистической блокировкой. После MoneyReceived gross неизменяем.
  • Гонка регистрации и отмены начисления. Начисление, отменённое из Confirmed (отмена сделки), ловится перепроверкой статуса на шаге 1 (REWARDS_RECEIPT_ITEM_INVALID) — оператор исключает позицию и корректирует сумму акта.
  • Уникальность номера. Number — из платформенного per-year sequence; при гонке уникальный индекс ux_platform_receipts_number страхует.
  • Ошибочно зарегистрированный акт. Не откатывается (INV-RC-4) — каскад уже разошёлся по кошелькам и апрувам; исправление — компенсирующее начисление / корректировка следующим периодом (двухшаговость Draft → Registered с предпросмотром состава и Σ снижает риск).
  • Оптимистическая блокировка переходов. Параллельные Register/Cancel/правка состава над одним актом разрешаются по Version: проходит один, проигравший перечитывает статус, повтор отклоняется guard-ом (REWARDS_RECEIPT_INVALID_STATUS_TRANSITION).

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

АспектЗначение
ИндексыPK; ux_platform_receipts_number unique btree; ix_platform_receipts_developer btree (developer_company_id, status); ix_platform_receipts_created_at btree; items: PK, ux_platform_receipt_items_accrual unique btree (commission_accrual_id), ix_platform_receipt_items_receipt btree, ix_platform_receipt_items_agency btree
Партиционирование / retentionnone — акты создаются по платежам застройщиков (единицы в неделю на застройщика); retention бессрочный (финансовый документ)
Concurrencyоптимистическая блокировка на переходах; включение позиций — под блокировкой начислений (INV-RC-1/3)
Permissions / PIIСоздание/состав/регистрация/отмена и чтение реестра — rewards.receipts.manage (платформенный scope, финансовый оператор ПИН — право независимо от прав арбитража сделок CRM, ADR-0067 п.5). Агентства и застройщики акты не видят — им виден эффект: статус MoneyReceived и PlatformReceiptId/MoneyReceivedAt своих начислений. Суммы — Sensitive-класс, в логи не пишутся. PII нет
AuditVersion + actor-stamping (RegisteredBy/CancelledBy) + доменные события; Registered-акт неизменяем — сам себе аудит-след

Наблюдаемость

  • Метрика возраста старейшего Confirmed-начисления без акта — rewards_accrual_awaiting_receipt_max_age_minutes (gauge) + алерт: оператор ПИН — узкое место, без регистрации акта выплаты заблокированы (ADR-0067, риски).
  • Счётчики регистраций/отмен актов; лог регистрации — Id, Number, DeveloperCompanyId, ItemsCount, id начислений (без сумм).

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

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

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