Entity / Aggregate: CommissionAccrualEntity
Дом полей агрегата «Начисление комиссии» контекста rewards. Все проекции (сводки ЛК агента и агентства,
payload событий, платёжные поручения) ссылаются сюда и описывают только дельту.
Назначение
CommissionAccrualEntity — начисление комиссионного вознаграждения по завершённой сделке CRM:
фиксирует, сколько агент (и его агентство) заработали на сделке, по какому правилу застройщика посчитана
сумма и в каком статусе подтверждения/выплаты она находится (матрица «Агенство» 5.1, 5.3, 5.6 — «какую
комиссию мы получим/получили: подтверждено / ожидает подтверждения»).
Создаётся consumer-ом события pin.crm.deal-completed.v1
(../consumers/crm-deal-completed.md): подбирается правило застройщика
(reward-rule.md, алгоритм подбора),
считается сумма CommissionAmount = База × CommissionPercent / 100, снимается snapshot правила и снапшот
условий платформы на момент завершения сделки — PlatformRetentionPercentagePoints / PlatformFeeAmount /
AppliedPlatformTermsId (platform-commission-terms.md, ADR-0066);
начисление создаётся в статусе PendingConfirmation. Подтверждает начисление застройщик (факт признания
обязательства по комиссии; опционально — с корректировкой суммы, INV-11); в момент подтверждения строится
распределение тремя строками CommissionDistributionEntity: Platform — удержание ПИН по снапшоту
условий, Agency и Agent — деление пула агентства (CommissionAmount − PlatformFeeAmount) по активной
политике (agency-commission-policy.md). Выплаты допускаются только после
подтверждения получения денег оператором ПИН — регистрации акта поступления средств
(platform-receipt.md, ADR-0067), переводящей начисление в MoneyReceived; далее —
платёжные поручения (payout-order.md; исполнение платежей отложено, решение №9 ТЗ).
Если правило застройщика для объекта недвижимости не найдено — начисление не создаётся: consumer логирует warning
с DealId, инкрементирует метрику rewards_accrual_no_rule_total; менеджер застройщика может создать
начисление вручную (admin-API rewards, карта областей — ../README.md). Если удержание ПИН
по применимым условиям платформы не меньше ставки применённого правила (пул агентства ≤ 0) — начисление
также не создаётся: событие уходит в DLQ REWARDS_PLATFORM_RETENTION_EXCEEDS_RULE на ручной разбор
(platform-commission-terms.md).
Классификация
| Поле | Значение |
|---|---|
| Контекст | rewards |
| Kind | AggregateRoot (+ дочерние Entity CommissionDistributionEntity, CommissionAccrualAdjustmentEntity и CommissionAdjustmentRequestEntity внутри границы агрегата) |
| Source of truth | rewards (суммы/статусы комиссии); сделка — реплика-ссылка на crm |
| Таблицы | rewards.commission_accruals, rewards.commission_distributions, rewards.commission_accrual_adjustments, rewards.commission_adjustment_requests |
| Tenant isolation | TenantId = CompanyId агентства-получателя (ADR-0052: tenant = владелец процесса; процесс получения/распределения/выплаты комиссии ведёт агентство). Сторона застройщика — явное поле DeveloperCompanyId + cross-tenant доступ по permission (см. «Безопасность») |
Поля (rewards.commission_accruals)
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Уникальный идентификатор начисления. |
TenantId | Guid | Да | index ix_commission_accruals_tenant_status (btree: tenant_id, status) | payload deal-completed (AgencyCompanyId) | Агентство-получатель комиссии. |
DealId | Guid | Да | unique ux_commission_accruals_deal_id — бизнес-идемпотентность consumer-а (одно начисление на сделку) | payload deal-completed | Ссылка на сделку crm (../../crm/domain/deal.md — по конвенции). Логическая, без FK. |
DeveloperCompanyId | Guid | Да | index ix_commission_accruals_developer (btree: developer_company_id, status) | payload | Застройщик — плательщик комиссии; подтверждающая сторона. |
AgentUserId | Guid | Да | index ix_commission_accruals_agent (btree: tenant_id, agent_user_id) | payload | Получатель комиссии = агент-исполнитель (Executor) на момент завершения сделки; на него ссылается строка Agent распределения (record-level доступ «свои начисления»). |
OriginAgentUserId | Guid | Да | index ix_commission_accruals_origin_agent (btree: tenant_id, origin_agent_user_id) | payload (deal-completed / fixation-approved) | Атрибуция: иммутабельный снапшот агента-инициатора сделки (кто зафиксировал/привёл инвестора) на момент события. Не получатель, а источник — для воронки (кому засчитать лид/фиксацию) и антифрода (сверка инициатор vs получатель). Обычно = AgentUserId, но расходится при передаче/переназначении сделки другому исполнителю до завершения. Не меняется при смене Executor. |
InvestorUserId | Guid? | Нет | — | payload | Инвестор-покупатель; только для трассировки, в ЛК не отдаётся агентству целиком (PII-правило ниже). |
PropertyObjectId | Guid | Да | index ix_commission_accruals_property_object_id | payload | Объект недвижимости (chessboard). |
DealCompletedAt | DateTimeOffset | Да | index ix_commission_accruals_deal_completed_at — периодные сводки ЛК | payload | Момент завершения сделки (из события). Периоды дашбордов считаются по нему. |
CalculationBase | CommissionCalculationBase | Да | хранится как text, varchar(32) | snapshot правила | База расчёта на момент начисления (reward-rule.md). |
CommissionPercent | decimal | Да | numeric(5,2), check > 0 AND <= 100 | snapshot правила | Применённый процент. |
BasePriceAmount | Money (owned) | Да | см. VO | payload | Базовая цена объекта недвижимости (до ремонта/скидки) на момент сделки. |
FinalPriceAmount | Money (owned) | Да | см. VO | payload | Конечная цена объекта недвижимости (базовая + ремонт) на момент сделки. |
CommissionAmount | Money (owned) | Да | см. VO; check commission_amount_value > 0 | вычисление | (CalculationBase == BasePrice ? BasePriceAmount : FinalPriceAmount).Value × CommissionPercent / 100, округление banker's до 2 знаков. После корректировки gross (INV-11) — скорректированное значение; исходное — в OriginalCommissionAmount. |
OriginalCommissionAmount | Money? (owned) | Нет | см. VO | app | Исходная автосчитанная сумма до корректировок; фиксируется один раз — при первой корректировке GrossAmount (INV-11). null — сумма не корректировалась. |
PlatformRetentionPercentagePoints | decimal | Да | numeric(4,2), check >= 0 AND <= 1.50 | snapshot условий платформы | Удержание ПИН в процентных пунктах ставки застройщика на момент DealCompletedAt (диапазон условий 0.25–1.50; 0 — fallback без условий, ADR-0066). |
PlatformFeeAmount | Money (owned) | Да | см. VO; check platform_fee_amount_value >= 0 | вычисление | база × PlatformRetentionPercentagePoints / 100 (INV-10), banker's, 2 знака; 0 при fallback. Пул агентства = CommissionAmount − PlatformFeeAmount. |
AppliedPlatformTermsId | Guid? | Нет (null — fallback 0 п.п.) | FK fk_commission_accruals_platform_terms → rewards.platform_commission_terms.id (restrict) | резолвер | Условия платформы (platform-commission-terms.md), применённые на момент DealCompletedAt. |
AppliedRuleId | Guid? | Нет (обязателен при автоначислении; null — ручное начисление) | FK fk_commission_accruals_rule → rewards.reward_rules.id (restrict), index | резолвер | Правило, по которому посчитана комиссия. |
AppliedRuleSnapshot | AppliedRuleSnapshotDocument (jsonb) | Да | jsonb, typed, ContractVersion | резолвер / ручной ввод | Snapshot условий расчёта на момент начисления — правило может быть потом изменено/архивировано. |
AppliedPolicyId | Guid? | Нет (заполняется при Confirm) | FK fk_commission_accruals_policy → rewards.agency_commission_policies.id (restrict) | Confirm | Политика агентства, по которой построено распределение. |
Status | CommissionAccrualStatus | Да | хранится как text, varchar(32) | app | Статус начисления (stateDiagram ниже). |
ConfirmedAt | DateTimeOffset? | Нет | заполняется при Confirm | app | Момент подтверждения застройщиком. |
ConfirmedByUserId | Guid? | Нет | — | app | Менеджер застройщика, подтвердивший начисление. |
MoneyReceivedAt | DateTimeOffset? | Нет | заполняется при регистрации акта | акт | Момент подтверждения получения денег (переход Confirmed → MoneyReceived). |
PlatformReceiptId | Guid? | Нет | FK fk_commission_accruals_platform_receipt → rewards.platform_receipts.id (restrict), index ix_commission_accruals_platform_receipt where not null | акт | Акт поступления средств (platform-receipt.md), подтвердивший получение денег по начислению. |
PaidAt | DateTimeOffset? | Нет | заполняется при MarkPaid | app | Момент выплаты (по платёжному поручению). |
CancelledAt | DateTimeOffset? | Нет | — | app | Момент отмены. |
CancelReason | CommissionCancelReason? | Нет (обязателен при Cancelled) | хранится как text, varchar(32) | app/consumer | DealCancelled (событие pin.crm.deal-cancelled.v1) / DeveloperRejected / ManualCorrection. |
CreatedAt | DateTimeOffset | Да | index | app | Момент создания (аудит). |
CreatedByUserId | Guid? | Нет | — | app | Система (автоначисление) / менеджер застройщика (ручное). |
UpdatedAt | DateTimeOffset | Да | — | app | Момент изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
Version | long | Да | >= 1 | app | Доменная версия (инкремент при мутации, попадает в события). |
Enum CommissionAccrualStatus
| Значение | Описание |
|---|---|
PendingConfirmation | Начислено по завершённой сделке, ожидает подтверждения застройщиком («ожидает подтверждения» в ЛК агента/агентства). |
Confirmed | Застройщик подтвердил обязательство (опционально скорректировав сумму, INV-11); построено распределение тремя строками Platform/Agency/Agent («подтверждено»). Деньги от застройщика ещё не получены — выплаты недоступны. |
MoneyReceived | Оператор ПИН подтвердил получение денег от застройщика: начисление вошло в зарегистрированный акт поступления средств (platform-receipt.md, ADR-0067). Гейт всех денежных следствий: Agent-строки доступны платёжным поручениям, связанное реферальное начисление авто-переходит в Approved (referral-accrual.md), кошельки получателей кредитуются. |
Paid | Все Agent-строки распределения выплачены (платёжное поручение в статусе Paid). Терминальный. |
Cancelled | Отменено (отмена/расторжение сделки, отказ застройщика, ручная отмена ошибочного начисления). Терминальный. |
Enum CommissionCancelReason
| Значение | Описание |
|---|---|
DealCancelled | Сделка отменена/расторгнута в CRM (consumer pin.crm.deal-cancelled.v1). |
DeveloperRejected | Застройщик отказал в подтверждении (обязательный комментарий в команде отказа). |
ManualCorrection | Ручная отмена ошибочного начисления целиком (admin-API, permission rewards.accruals.manage). Не способ поправить сумму: правка сумм/долей — first-class корректировками (INV-11, CommissionAccrualAdjustmentEntity). |
Дочерняя сущность CommissionDistributionEntity (rewards.commission_distributions)
Строка распределения подтверждённой комиссии между получателями. Создаётся при подтверждении (Confirm)
— ровно три строки (INV-4/INV-9): Platform — удержание ПИН (= PlatformFeeAmount, по снапшоту
условий платформы, ADR-0066), Agency — удержание агентства из пула, Agent — остаток пула; пул
агентства = CommissionAmount − PlatformFeeAmount, деление пула — по активной политике агентства
(agency-commission-policy.md). Сумма трёх строк равна CommissionAmount
(INV-5). Kind: Entity внутри границы агрегата CommissionAccrualEntity — все мутации только через
корень.
| Поле | Тип | Обязательность | Ограничения | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор строки. |
AccrualId | Guid | Да | FK fk_commission_distributions_accrual → commission_accruals.id (cascade), index ix_commission_distributions_accrual | app | Родительское начисление. |
TenantId | Guid | Да | index (tenant_id, recipient_user_id) | родитель | Дублируется для изоляции по tenant. |
RecipientType | CommissionRecipientType | Да | хранится как text, varchar(16): Platform / Agent / Agency | распределение | Тип получателя. |
RecipientUserId | Guid? | Да для Agent, null для Agency и Platform | check (recipient_type = 'Agent') = (recipient_user_id is not null) | родитель (AgentUserId) | Пользователь-получатель доли (только строка Agent). |
Amount | Money (owned) | Да | check amount_value >= 0 | вычисление (снапшот условий платформы / политика) | Сумма доли. |
AppliedRetentionKind | AgencyRetentionKind? | Только для строки Agency | хранится как text, varchar(16) | политика | Как считалось удержание агентства: Percent / FixedAmount (см. политику). |
PayoutOrderItemId | Guid? | Нет | FK fk_commission_distributions_payout_item → rewards.payout_order_items.id (set null), index where not null | payout | Заполняется при включении строки в платёжное поручение (только строки Agent); null — строка свободна. Строки Platform/Agency в поручения не включаются. |
PaidAt | DateTimeOffset? | Нет | — | payout | Момент выплаты (проставляется при Paid поручения). |
CreatedAt / UpdatedAt | DateTimeOffset | Да | — | app | Аудит строки. |
Enum CommissionRecipientType
| Значение | Описание |
|---|---|
Platform | Удержание ПИН как площадки (= PlatformFeeAmount, ADR-0066). RecipientUserId = null — получатель не персона, а платформа. Неприкосновенна для агентства (INV-11): меняется только пересчётом при корректировке gross; в платёжные поручения не включается — по событию получения денег кредитуется счёт платформы в кошельке. |
Agent | Доля агента-исполнителя сделки из пула агентства (выплачивается по платёжному поручению после MoneyReceived). |
Agency | Удержание в пользу агентства из пула (не выплачивается агенту; остаётся доходом агентства). |
Co-broking (ADR-0069). В v1 у сделки один финансовый получатель — назначенный
Executor(ADR-0068): распределение строго трёхстрочное, строкаAgentровно одна (INV-4/5/9), прочие агентские участники (CoAgent/Assistant/Lawyer) нефинансовы. Мультистрочное агентское распределение (несколько строкAgentс долями изAgentShares[]payload завершения сделки, обобщение INV-4/5/9 на1..NстрокAgent) — v-next, вне текущего контракта. Заявочная схема «агент запрашивает → руководитель одобряет» (CommissionAdjustmentRequestEntity) в v1 оперирует единственнойAgent-строкой; при переходе на co-broking заявка обобщится на конкретную строку получателя.
Дочерняя сущность CommissionAccrualAdjustmentEntity (rewards.commission_accrual_adjustments)
Аудит-строка корректировки начисления (ADR-0067 п.4): кто, когда, что и почему изменил. Append-only —
строки не редактируются и не удаляются; каждая корректировка суммы (GrossAmount) или долей
(Distribution) обязана оставить ровно одну строку. Kind: Entity внутри границы агрегата
CommissionAccrualEntity — создаётся только операциями корректировки корня (INV-11).
| Поле | Тип | Обязательность | Ограничения | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор строки корректировки. |
AccrualId | Guid | Да | FK fk_commission_accrual_adjustments_accrual → commission_accruals.id (cascade), index ix_commission_accrual_adjustments_accrual | app | Родительское начисление. |
TenantId | Guid | Да | index | родитель | Дублируется для изоляции по tenant. |
Kind | CommissionAdjustmentKind | Да | хранится как text, varchar(16): GrossAmount / Distribution | app | Что корректировалось (enum ниже). |
OldAmount | Money (owned) | Да | см. VO | app | Значение до корректировки: для GrossAmount — CommissionAmount, для Distribution — сумма строки Agent. |
NewAmount | Money (owned) | Да | check new_amount_value >= 0 | app | Значение после корректировки (та же семантика, что у OldAmount). |
Reason | string | Да | varchar(1000), min 1 — обязательная причина | app | Человекочитаемая причина корректировки (аудит, отчётность по корректировкам). |
ActorUserId | Guid | Да | — | app | Актор: менеджер застройщика (корректировка при подтверждении) / ПИН-админ (gross) / руководитель агентства (distribution). |
CreatedAt | DateTimeOffset | Да | index ix_commission_accrual_adjustments_created_at | app | Момент корректировки. |
Enum CommissionAdjustmentKind
| Значение | Описание |
|---|---|
GrossAmount | Корректировка суммы начисления CommissionAmount (застройщик — в составе подтверждения; ПИН-админ — в PendingConfirmation/Confirmed, INV-11). При первой корректировке исходная сумма фиксируется в OriginalCommissionAmount; в Confirmed пересчитывается распределение: строка Platform — по снапшоту PlatformRetentionPercentagePoints, пул — по применённой политике. |
Distribution | Per-deal перераспределение пула агентства между строками Agency и Agent (руководитель агентства, INV-11). Строка Platform не меняется; CommissionAmount не меняется. |
Дочерняя сущность CommissionAdjustmentRequestEntity (rewards.commission_adjustment_requests)
Заявка агента на корректировку своей доли (Agent-строки) в пуле сделки по схеме «агент запрашивает →
руководитель одобряет». Агент не правит распределение напрямую (у него нет права распоряжаться пулом
агентства) — он предлагает новую сумму своей доли ProposedAgentAmount с обоснованием; руководитель
агентства одобряет (тогда предложение исполняется как Distribution-корректировка — те же строки Agency/
Agent, что и при прямой правке) либо отклоняет. Kind: Entity внутри границы агрегата
CommissionAccrualEntity — создаётся и разрешается только операциями корня (INV-12); аудит-строку
CommissionAccrualAdjustmentEntity (Kind = Distribution) создаёт именно исполнение при одобрении, а не сама
заявка. На начисление одновременно допускается не более одной заявки в статусе Pending (INV-12).
Заявочные операции: ../api/agent/request-distribution-adjustment.md
(агент создаёт Pending), ../api/agency/approve-distribution-adjustment.md
(руководитель одобряет/отклоняет; одобрение исполняет корректировку той же логикой, что
../api/agency/adjust-accrual-distribution.md).
| Поле | Тип | Обязательность | Ограничения | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор заявки. |
AccrualId | Guid | Да | FK fk_commission_adjustment_requests_accrual → commission_accruals.id (cascade), index ix_commission_adjustment_requests_accrual; partial unique ux_commission_adjustment_requests_one_pending (accrual_id) where status = 'Pending' — одна открытая заявка на начисление (INV-12) | app | Родительское начисление. |
TenantId | Guid | Да | index (tenant_id, status) | родитель | Дублируется для изоляции по tenant. |
RequestedByAgentUserId | Guid | Да | index ix_commission_adjustment_requests_agent (tenant_id, requested_by_agent_user_id) | claims (sub) | Агент-заявитель; на момент создания = AgentUserId начисления (получатель Agent-строки, record-level). |
ProposedAgentAmount | Money (owned) | Да | см. VO; check proposed_agent_amount_value >= 0 | заявка | Предлагаемая сумма доли агента (Agent-строки). При исполнении доля Agency = пул − ProposedAgentAmount. |
Reason | string | Да | varchar(1000), min 1 — обязательное обоснование | заявка | Человекочитаемое обоснование предложения (переносится в Reason аудит-строки Distribution при одобрении). |
Status | CommissionAdjustmentRequestStatus | Да | хранится как text, varchar(16) | app | Статус заявки (enum ниже). |
ResolvedByUserId | Guid? | Нет (обязателен при Approved/Rejected) | — | app | Руководитель агентства, разрешивший заявку. |
ResolvedAt | DateTimeOffset? | Нет (заполняется при разрешении) | — | app | Момент одобрения/отклонения. |
ResolutionComment | string? | Нет | varchar(1000) | app | Комментарий руководителя (обоснование отклонения; опционально при одобрении). |
AppliedAdjustmentId | Guid? | Нет (заполняется при Approved) | FK fk_commission_adjustment_requests_adjustment → commission_accrual_adjustments.id (set null) | app | Аудит-строка Distribution, созданная исполнением одобренной заявки (трассировка «заявка → корректировка»). |
CreatedAt | DateTimeOffset | Да | index ix_commission_adjustment_requests_created_at | app | Момент подачи заявки. |
UpdatedAt | DateTimeOffset | Да | — | app | Момент изменения. |
Enum CommissionAdjustmentRequestStatus
| Значение | Описание |
|---|---|
Pending | Заявка подана агентом, ожидает решения руководителя. Единственная открытая заявка на начисление (partial unique). |
Approved | Руководитель одобрил: предложение исполнено как Distribution-корректировка (Agent = ProposedAgentAmount, Agency = пул − ProposedAgentAmount); создана аудит-строка CommissionAccrualAdjustmentEntity (AppliedAdjustmentId). Терминальный. |
Rejected | Руководитель отклонил: распределение не изменено, ResolutionComment — причина отказа. Терминальный. |
Value objects
Money (единый VO контекста rewards)
| Поле | Тип | Обязательность | Ограничения | Колонка (пример для CommissionAmount) |
|---|---|---|---|---|
Value | decimal | Да | numeric(18,2), >= 0 | commission_amount_value |
CurrencyId | short | Да | ссылка на валюту справочника (../../handbook/domain/currency.md, логическая, без FK); дефолт — RUB; допустимость валют — RUB-only (ADR-0048) | commission_amount_currency_id |
Инвариант VO: операции сложения/вычитания допустимы только при равном CurrencyId
(REWARDS_CURRENCY_MISMATCH). Все Money-поля одного начисления — с одним CurrencyId (INV-6).
В payload событий денежные суммы передаются как Value + CurrencyId + CurrencySnapshot —
см. ../../handbook/domain/snapshots.md (справочник валют ≠
мультивалютность).
Модели JSONB (versioned)
AppliedRuleSnapshotDocument (jsonb, колонка applied_rule_snapshot, ContractVersion = 1)
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
ContractVersion | int | Да | = 1 | v1 |
RuleId | Guid? | Нет (null — ручное начисление) | — | v1 |
RuleName | string | Да | varchar(256); для ручного — "manual" | v1 |
RuleVersion | long? | Нет | Version правила на момент применения | v1 |
CalculationBase | CommissionCalculationBase (enum-string) | Да | — | v1 |
CommissionPercent | decimal | Да | > 0, <= 100, scale 2 | v1 |
Priority | int? | Нет | приоритет применённого правила | v1 |
MatchedConditions | RewardRuleConditionsDocument | Да | копия условий правила на момент применения (reward-rule.md) | v1 |
PropertyObjectPropertyType | PropertyType (enum-string) | Да | атрибут объекта недвижимости из события | v1 |
PropertyObjectLayoutId | Guid? | Нет | — | v1 |
PropertyObjectLayoutGroupId | Guid? | Нет | — | v1 |
PropertyObjectRooms | int? | Нет | 0..20 | v1 |
PropertyObjectAreaTotalM2 | decimal | Да | > 0, scale 2 | v1 |
Правила версии: additive-поля — той же версией (nullable); смена семантики — ContractVersion + 1.
Связи
| Связь | Тип | Ключ | Delete behavior |
|---|---|---|---|
CommissionDistributionEntity | owned (внутри агрегата) | AccrualId | cascade |
CommissionAccrualAdjustmentEntity | owned (внутри агрегата) | AccrualId | cascade |
CommissionAdjustmentRequestEntity | owned (внутри агрегата) | AccrualId | cascade |
RewardRuleEntity | reference (nullable) | AppliedRuleId | restrict |
AgencyCommissionPolicyEntity | reference (nullable) | AppliedPolicyId | restrict |
PlatformCommissionTermsEntity | reference (nullable) | AppliedPlatformTermsId | restrict |
PlatformReceiptEntity | reference (nullable) | PlatformReceiptId | restrict |
PayoutOrderItemEntity | reference (со стороны строки распределения) | PayoutOrderItemId | set null (отмена поручения освобождает строки) |
Сделка crm | external reference (без FK) | DealId | — (только хранение ссылки; данные — из payload события) |
Объект недвижимости chessboard | external reference (без FK) | PropertyObjectId | — (только хранение ссылки) |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-1 OneAccrualPerDeal | На DealId существует не более одного начисления (идемпотентность consumer-а) | unique index + проверка существующего начисления до создания | дубль события — идемпотентный skip |
| INV-2 AmountFormula | CommissionAmount = база × CommissionPercent / 100 (banker's rounding, 2 знака); база выбирается по CalculationBase | метод Create/CreateManual агрегата | REWARDS_ACCRUAL_AMOUNT_MISMATCH |
| INV-3 ConfirmGuard | Confirm допустим только из PendingConfirmation; актор — менеджер компании DeveloperCompanyId | метод агрегата + permission-policy | REWARDS_ACCRUAL_INVALID_STATUS_TRANSITION |
| INV-4 DistributionOnConfirm | Строки распределения создаются ровно при Confirm: Platform — по снапшоту условий платформы, Agency/Agent — делением пула по активной политике агентства; до подтверждения строк нет | метод Confirm | REWARDS_ACCRUAL_DISTRIBUTION_EXISTS |
| INV-5 DistributionSum | Σ Distribution.Amount.Value = CommissionAmount.Value по трём строкам: Platform = PlatformFeeAmount; Agency + Agent = пул (CommissionAmount − PlatformFeeAmount); при FixedAmount-удержании больше пула — удержание агентства срезается до пула, доля агента = 0 | метод Confirm (расчёт) + проверка суммы | REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID |
| INV-6 SingleCurrency | BasePriceAmount.CurrencyId = FinalPriceAmount.CurrencyId = CommissionAmount.CurrencyId = CurrencyId всех строк распределения и корректировок | метод Create + VO-guard | REWARDS_CURRENCY_MISMATCH |
| INV-7 PaidOnlyViaPayout | Paid достижим только из MoneyReceived и выставляется только обработчиком перехода поручения в Paid (payout-order.md), когда у всех строк Agent-распределения PaidAt != null | метод MarkPaid (internal) | REWARDS_ACCRUAL_PAID_ONLY_VIA_PAYOUT |
| INV-8 CancelGuard | Cancel допустим из PendingConfirmation и из Confirmed — если ни одна строка не включена в неотменённое поручение (PayoutOrderItemId is null у всех) и начисление не входит в не-отменённый акт поступления (platform-receipt.md). Из MoneyReceived и Paid — запрещён: после получения денег ошибки лечатся только компенсирующим ручным начислением, не разворотом статусов (ADR-0067) | метод Cancel | REWARDS_ACCRUAL_CANCEL_FORBIDDEN |
| INV-9 RecipientConsistency | Строк ровно три — по одной каждого типа Platform/Agency/Agent; строка Agent ссылается на AgentUserId начисления; у Platform и Agency RecipientUserId = null | метод Confirm + db check | REWARDS_ACCRUAL_DISTRIBUTION_INVALID |
| INV-10 PlatformFeeFormula | PlatformFeeAmount = база × PlatformRetentionPercentagePoints / 100 (banker's rounding, 2 знака; база — по CalculationBase, та же, что в INV-2); пул агентства = CommissionAmount − PlatformFeeAmount; политика агентства делит только пул (agency-commission-policy.md). При корректировке gross PlatformFeeAmount не меняется (считается от базы, а не от комиссии); пересчитываются пул и его деление | метод Create + пересчёт распределения при корректировке gross | REWARDS_ACCRUAL_PLATFORM_FEE_MISMATCH |
| INV-11 AdjustmentGuards | GrossAmount: застройщик — только в составе подтверждения (AdjustedAmount + причина в команде confirm); ПИН-админ (permission rewards.accruals.adjust) — в PendingConfirmation | Confirmed, пока ни одна строка не включена в неотменённое поручение и начисление не входит в не-отменённый акт; скорректированная сумма > PlatformFeeAmount (пул > 0); после MoneyReceived gross неизменяем — только компенсирующее начисление. Distribution: перераспределяется только пул Agency ↔ Agent, строка Platform неприкосновенна. Инициатор Distribution — один из: руководитель агентства (permission rewards.commission.adjust-approve — прямая правка или одобрение заявки) в Confirmed | MoneyReceived, пока строка Agent не включена в поручение; агент (permission rewards.commission.adjust-request) — только предложение через CommissionAdjustmentRequestEntity (Pending), не прямая правка: агент не распоряжается пулом, изменение строк выполняется лишь одобрением руководителя (исполнение — INV-12). Каждая исполненная корректировка создаёт строку CommissionAccrualAdjustmentEntity с обязательной Reason (для одобренной заявки Reason берётся из заявки, ActorUserId = одобривший руководитель) | операции корректировки + permission-policy | REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN; пустая причина — REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED |
| INV-12 AdjustmentRequestLifecycle | Заявка агента (CommissionAdjustmentRequestEntity): создаётся агентом-получателем (RequestedByAgentUserId = AgentUserId начисления) только когда начисление ∈ {Confirmed, MoneyReceived}, строка Agent свободна (PayoutOrderItemId is null) и по начислению нет другой Pending-заявки (partial unique); ProposedAgentAmount в [0; пул] той же валюты (INV-6). Разрешает заявку руководитель: одобрение допустимо только из Pending и исполняет Distribution-корректировку по актуальному пулу (Agent = ProposedAgentAmount, Agency = пул − ProposedAgentAmount, INV-5/10) под теми же guard-ами, что INV-11 (Agent-строка всё ещё свободна, статус ∈ {Confirmed, MoneyReceived}); при изменившемся пуле ProposedAgentAmount > пул — отказ. Отклонение из Pending не меняет распределение. Approved/Rejected — терминальны | заявочные операции + permission-policy | REWARDS_ADJUSTMENT_REQUEST_FORBIDDEN; активная заявка уже есть — REWARDS_ADJUSTMENT_REQUEST_ALREADY_PENDING; заявка не в Pending — REWARDS_ADJUSTMENT_REQUEST_INVALID_STATUS |
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
CommissionAccruedEvent | доменное | Успешный Create | Id, DealId, CommissionAmount |
CommissionConfirmedEvent | доменное | PendingConfirmation → Confirmed | Id, распределение |
CommissionAccrualCreatedKafkaEvent | интеграционное (ContractVersion = 1, topic pin.rewards.commission-accrual-created.v1, key = Id, AggregateId = Id) | После Create | Id, TenantId, DealId, DeveloperCompanyId, AgentUserId, PropertyObjectId, CommissionAmount (Value + CurrencyId + CurrencySnapshot), PlatformFeeAmount, PlatformRetentionPercentagePoints, Status, DealCompletedAt. Контракт — ../events/commission-accrual-created.md. Потребители: notifications (уведомление агенту/агентству), crm (отметка в таймлайне сделки) |
CommissionAccrualStatusChangedKafkaEvent | интеграционное (ContractVersion = 1, topic pin.rewards.commission-accrual-status-changed.v1, key = Id) | Каждый переход Status | Id, TenantId, DealId, FromStatus, ToStatus, CancelReason?, ConfirmedAt?, MoneyReceivedAt?, PaidAt?, Version. Потребители: notifications |
CommissionMoneyReceivedKafkaEvent | интеграционное (ContractVersion = 1, topic pin.rewards.commission-money-received.v1, key = Id) | Confirmed → MoneyReceived (регистрация акта, platform-receipt.md) | Id, TenantId, DealId, DeveloperCompanyId, PlatformReceiptId, MoneyReceivedAt, CommissionAmount (Value + CurrencyId + CurrencySnapshot), Distributions[] (RecipientType, RecipientUserId?, Amount), Version. Контракт — ../events/commission-money-received.md. Потребители: wallet (кредиты счетов получателей), notifications |
CommissionAdjustmentRequestedEvent | доменное | Агент создал заявку (CommissionAdjustmentRequestEntity → Pending) | RequestId, AccrualId, RequestedByAgentUserId, ProposedAgentAmount |
CommissionAdjustmentRequestResolvedEvent | доменное | Руководитель одобрил/отклонил заявку (Pending → Approved/Rejected) | RequestId, AccrualId, Status, ResolvedByUserId, AppliedAdjustmentId? |
CommissionAdjustmentRequestStatusChangedKafkaEvent | интеграционное (ContractVersion = 1, topic pin.rewards.commission-adjustment-request-status-changed.v1, key = AccrualId) | Создание заявки и её разрешение | RequestId, AccrualId, TenantId, RequestedByAgentUserId, Status (Pending/Approved/Rejected), ResolvedByUserId?, ResolutionComment?, CreatedAt/ResolvedAt?. Контракт — ../events/commission-adjustment-request-status-changed.md (будущий путь по конвенции). Потребители: notifications (уведомление руководителю о новой заявке / агенту о решении) |
Исполнение одобренной заявки публикует также pin.rewards.commission-distribution-adjusted.v1 (при
MoneyReceived — компенсация кошелька, ../events/commission-distribution-adjusted.md),
как и прямая Distribution-корректировка. Интеграционные события публикуются атомарно со сменой состояния
агрегата.
Конкурентность, идемпотентность и восстановление
- Идемпотентность создания. На
DealId— не более одного начисления (INV-1): повтор событияdeal-completed/гонка двух инстансов consumer-а упирается в uniqueux_commission_accruals_deal_idи трактуется как идемпотентный skip (перечитать → запись есть). Ручное начисление использует ту же защиту поDealId. - Гонка
ConfirmvsCancel. Оба перехода стартуют изPendingConfirmation; оптимистическая блокировка (Version) пропускает ровно один — проигравший получает конфликт версии и перечитывает уже изменённый статус; повтор его перехода отклоняется guard-ом (INV-3/INV-8,REWARDS_ACCRUAL_INVALID_STATUS_TRANSITION/REWARDS_ACCRUAL_CANCEL_FORBIDDEN). - Гонка
Cancelvs включение в поручение.CancelизConfirmedдопустим только пока всеAgent-строки свободны (PayoutOrderItemId is null, INV-8); захват строк поручением (payout-order.md, INV-1) идёт под блокировкой строк — победитель первым проставляетPayoutOrderItemId, после чегоCancelуже недоступен (проигравший видитREWARDS_ACCRUAL_CANCEL_FORBIDDEN). - Гонка
Cancelvs включение в акт поступления.CancelизConfirmedтакже недоступен, пока начисление входит в не-отменённый акт (platform-receipt.md); включение позиции в акт и отмена мутируют один агрегат — оптимистическая блокировка пропускает ровно одну операцию, проигравший перечитывает состояние. - Переход в
MoneyReceived. Выполняется только регистрацией акта поступления средств — атомарно для всех позиций акта вместе с авто-Approveсвязанных реферальных начислений и публикациейcommission-money-received; прямой перевод статуса извне не предусмотрен. - Переход в
Paid. Достижим только изMoneyReceivedи выставляется только каскадом поручения (INV-7), когда у всехAgent-строкPaidAt != null; прямойMarkPaidизвне запрещён (REWARDS_ACCRUAL_PAID_ONLY_VIA_PAYOUT). - Гонка «корректировка распределения ↔ сборка поручения». Guard INV-11 «строка
Agentне включена в поручение» + оптимистическая блокировка начисления: обе операции мутируют один агрегат, проходит одна; проигравший перечитывает актуальное состояние и повторяет решение. - Компенсации после получения денег. Из
MoneyReceivedиPaidотмена и правка gross запрещены (INV-8, INV-11); ошибочно подтверждённое/выплаченное начисление компенсируется отдельным ручным корректирующим начислением, а не правкой существующего — финансовая история неизменяема,Registered-акт не разворачивается (ADR-0067).
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK; ux_commission_accruals_deal_id unique btree; ix_commission_accruals_tenant_status btree (tenant_id, status); ix_commission_accruals_agent btree (tenant_id, agent_user_id); ix_commission_accruals_origin_agent btree (tenant_id, origin_agent_user_id); ix_commission_accruals_developer btree (developer_company_id, status); ix_commission_accruals_deal_completed_at btree; ix_commission_accruals_property_object_id btree; ix_commission_accruals_platform_receipt btree where not null; distributions: PK, ix_commission_distributions_accrual btree, ix_commission_distributions_payout_item btree where not null, ix_commission_distributions_recipient btree (tenant_id, recipient_user_id); adjustments: PK, ix_commission_accrual_adjustments_accrual btree, ix_commission_accrual_adjustments_created_at btree (отчётность по корректировкам); adjustment_requests: PK, ix_commission_adjustment_requests_accrual btree, ux_commission_adjustment_requests_one_pending partial unique (accrual_id) where status='Pending', ix_commission_adjustment_requests_agent btree (tenant_id, requested_by_agent_user_id), ix_commission_adjustment_requests_created_at btree |
| Партиционирование / retention | none — объём = число завершённых сделок (десятки тысяч в год, ADR-0056); retention бессрочный (финансовая история) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую): подтверждение застройщика vs отмена vs включение в поручение |
| Permissions / PII | Агент — только свои начисления (record-level AgentUserId, permission rewards.commission.view-own) и заявка на корректировку своей доли (rewards.commission.adjust-request — только предложение, INV-12); руководитель агентства — все начисления tenant-а (rewards.commission.view-all, scope = организация), прямая правка долей и одобрение заявок (rewards.commission.adjust-approve); застройщик — начисления с его DeveloperCompanyId (cross-tenant чтение/подтверждение по permission rewards.accruals.confirm, фильтр по DeveloperCompanyId — вне обычной изоляции по tenant, отдельный путь чтения); Менеджер ПИН — платформенный scope. InvestorUserId в проекции агентства маскируется (отдаётся только DealId); суммы — Sensitive-класс, в логи не пишутся |
| Audit | Version + actor-stamping + события статусов; корректировки сумм и долей — first-class строками CommissionAccrualAdjustmentEntity (обязательная причина + актор, append-only); заявки агентов на корректировку доли — CommissionAdjustmentRequestEntity (заявитель + резолвер + AppliedAdjustmentId, трассировка «заявка → корректировка»); остальная история — событиями |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (9): Entity / Aggregate: AgencyCommissionPolicyEntity, Entity / Aggregate: LedgerTransactionEntity (+ LedgerEntryEntity), Entity / Aggregate: PayoutOrderEntity, Entity / Aggregate: PlatformCommissionTermsEntity, Entity / Aggregate: PlatformReceiptEntity, Entity / Aggregate: ReferralAccrualEntity, Entity / Aggregate: RewardRuleEntity, Entity / Aggregate: WalletAccountEntity, Entity / Projection: AgentPerformanceEntity
- API (32): GET /api/v1/agency/agents/{agentUserId}/commission-summary — Сводка по конкретному агенту, GET /api/v1/agency/commission-accruals — Список начислений агентства, GET /api/v1/agency/commission-summary — Сводка комиссий агентства, GET /api/v1/agency/wallet/agent-balances — Балансы агентов агентства, GET /api/v1/developer/commission-accruals — Начисления комиссий для застройщика, GET /api/v1/my/commission-accruals — Мои начисления (агент), GET /api/v1/my/commission-summary — Моя сводка комиссий (агент), GET /api/v1/platform/commission-terms — Реестр условий платформенной комиссии, POST /api/v1/agency/agent-boost-campaigns — Создание boost-кампании агентов (черновик), POST /api/v1/agency/commission-accruals/{accrualId}/adjust-distribution — Per-deal корректировка долей внутри пула, POST /api/v1/agency/commission-adjustment-requests/{requestId}/resolve — Решение по заявке агента на корректировку доли, POST /api/v1/agency/commission-policies — Создание комиссионной политики агентства (черновик) (+20)
- Use Cases (19): UC-CRM-009. Передача ведения сделки (смена исполнителя), UC-CRM-015. Явное назначение исполнителя для разрешения спора о комиссии, UC-RWD-002. Агент видит комиссию на объекте недвижимости в каталоге, UC-RWD-003. Сделка завершена — начисление комиссии (system), UC-RWD-004. Застройщик подтверждает начисление комиссии, UC-RWD-005. Агентство настраивает распределение и формирует платёжное поручение, UC-RWD-007. Платформа настраивает условия удержания ПИН, UC-RWD-010. Акт поступления средств — гейт получения денег, UC-RWD-011. Корректировки начисления комиссии, UC-RWD-012. Владелец агентства выгружает отчёты по вознаграждениям в Excel, UC-RWD-013. Агент предлагает корректировку своей доли, руководитель одобряет, UC-RWD-015. Настройка индивидуальных условий вознаграждения по области и сроку (акции и boost) (+7)
- События (8): Event Contract: pin.rewards.commission-accrual-created, Event Contract: pin.rewards.commission-accrual-status-changed (CommissionAccrualStatusChangedKafkaEvent), Event Contract: pin.rewards.commission-distribution-adjusted (CommissionDistributionAdjustedKafkaEvent), Event Contract: pin.rewards.commission-money-received (CommissionMoneyReceivedKafkaEvent), Event Contract: pin.rewards.payout-order-status-changed (PayoutOrderStatusChangedKafkaEvent), Event Contract: pin.rewards.referral-accrual-created (ReferralAccrualCreatedKafkaEvent), Event Contract: pin.rewards.referral-accrual-status-changed (ReferralAccrualStatusChangedKafkaEvent), Event Contract: pin.wallet.transaction-recorded (WalletTransactionRecordedKafkaEvent)
- Консюмеры (5): Consumer: Agent Performance Projection, Consumer: CRM Deal Cancelled, Consumer: CRM Deal Completed, Consumer: Rewards Commission Distribution Adjusted, Consumer: Rewards Commission Money Received
- Саги (1): Saga «Commission Settlement» (финансовый контур вознаграждений)
- Отчёты (2): Отчет/Export: Вознаграждения агентства по сотрудникам и сделкам (agency-rewards-by-employee-per-deal), Отчет/Export: Сводка вознаграждений агентства за период (agency-rewards-period-summary)
- adr (4): ADR-0065 — контекст wallet (внутренние балансы и ledger), ADR-0066 — удержание ПИН и реферальные ставки, ADR-0067 — гейт получения денег (акт поступления средств), ADR-0069: Co-broking — расщепление комиссии между несколькими агентами (дизайн, v-next)
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
- index.md (2): Rewards Service, Wallet Service
Связанные документы
- Consumer-источник: ../consumers/crm-deal-completed.md,
отмена —
../consumers/crm-deal-cancelled.md(по конвенции). - Контракты производимых событий: ../events/commission-accrual-created.md, ../events/commission-accrual-status-changed.md, ../events/commission-money-received.md.
- Событие-источник:
../../crm/events/deal-completed.md(будущий путь по конвенции, topicpin.crm.deal-completed.v1). - reward-rule.md — правило и алгоритм подбора; agency-commission-policy.md — деление пула агентства; payout-order.md — выплата.
- platform-commission-terms.md — условия удержания ПИН (снапшот в начисление);
platform-receipt.md — акт поступления средств (переход
MoneyReceived); referral-accrual.md — реферальное начисление (авто-ApprovedпоMoneyReceived). - Snapshot-модели справочника (деньги в событиях): ../../handbook/domain/snapshots.md.
- Заявки агента на корректировку доли: ../api/agent/request-distribution-adjustment.md
(создание
Pending), ../api/agency/approve-distribution-adjustment.md (одобрение/отклонение; одобрение исполняет корректировку логикой ../api/agency/adjust-accrual-distribution.md). - ADR-0052 (tenancy, two-sided сущности), ADR-0056 (simplicity-first), ADR-0066 (удержание ПИН, три строки
распределения), ADR-0067 (гейт получения денег, корректировки), ADR-0069 (co-broking, мультистрочный
Agent— v-next). - Шаблон: entity-template.md; правила: cross-cutting-document-requirements.md.