Consumer: CRM Deal Completed
Назначение
Consumer слушает событие завершения сделки CRM (Sale → Completed, deal.md) и
запускает финансовый контур вознаграждений — два независимых эффекта одного события:
- Начисление комиссии агенту/агентству: подбирает правило вознаграждения застройщика по атрибутам
объекта недвижимости, считает gross
CommissionAmount = база × CommissionPercent / 100, резолвит условия платформы на момент завершения сделки и снимает снапшот удержания ПИН (PlatformRetentionPercentagePoints/PlatformFeeAmount/AppliedPlatformTermsId— platform-commission-terms.md, ADR-0066), снимает snapshot применённого правила и создаёт начисление (CommissionAccrualEntity) в статусеPendingConfirmation(матрица «Агенство» 5.1/5.3 — комиссия ожидает подтверждения). Далее начисление подтверждает застройщик (../api/admin/confirm-commission-accrual.md, карта областей — ../README.md). - Реферальное начисление партнёру: если у инвестора-покупателя есть активная реферальная связь
(реплика referral-bond.md), резолвится ставка
(referral-rate-rule.md) и создаётся
ReferralAccrualEntityв статусеPending(guard самореферала — INV-RA-4). Ветка независима от наличия агента и правила застройщика: рефералка без родительского комиссионного начисления остаётсяPendingдо его появления (CommissionAccrualId = null).
Идемпотентность — по DealId для обеих веток: одна сделка порождает не более одного комиссионного
(unique ux_commission_accruals_deal_id, INV-1) и не более одного реферального начисления
(unique ux_referral_accruals_deal_id, INV-RA-1). Оба эффекта фиксируются в одном согласованном изменении.
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | rewards (фоновый обработчик) |
| Обрабатываемое событие | pin.crm.deal-completed |
| Topic | pin.crm.deal-completed.v1 |
| Partitions / key | key = DealId — строгий порядок per-deal |
| ContractVersion | 1; при несовпадении версии — warn-and-proceed (метрика rewards_contract_mismatch_total) |
| Consumer group | pin-rewards-crm-deals (общая с consumer-ом отмены — crm-deal-cancelled.md, документ по конвенции) |
| Producer | crm (переход Sale → Completed) |
| Эффект (событие → что меняется) | создание начисления комиссии (CommissionAccrualEntity в PendingConfirmation, со снапшотом условий платформы) + создание реферального начисления (ReferralAccrualEntity в Pending) при активной связи инвестора |
Контекст события
- Из метаданных события доступны: идентификатор сообщения, время события,
Version, агрегат-источник (AggregateId = DealId,AggregateType = "Deal"). - Корреляция сквозного трейса восстанавливается из метаданных события и пробрасывается в исходящее событие
rewards(шаг 6). - Обработка — под системным актором вне tenant-контекста (platform-scope):
CreatedByUserIdначисления штампуется системным актором;TenantIdначисления берётся из payload (OriginAgencyCompanyId— агентство-основатель сделки), а не из контекста актора (ADR-0052, решение №2 README). - Ключ идемпотентности —
rewards:crm-deal-completed:{MessageId}; бизнес-идемпотентность — уникальностьDealIdв обеих ветках (INV-1 комиссионного, INV-RA-1 реферального начисления).
Входные данные / payload события
Контракт события — ../../crm/events/deal-completed.md (дом контракта
— контекст crm): событие несёт атрибуты объекта недвижимости и обе цены (базовую и конечную) — без
них расчёт по CalculationBase невозможен. Денежные поля — Amount + CurrencyId + CurrencySnapshot
(../../handbook/domain/snapshots.md); CurrencySnapshot consumer-ом
не читается (display-модель).
| Поле payload | Тип | Обязательность | Как используется (маппинг) |
|---|---|---|---|
DealId | Guid | Да | Бизнес-ключ идемпотентности → CommissionAccrualEntity.DealId (unique) |
TenantId | Guid | Да | = DeveloperCompanyId сделки (crm tenant = застройщик) → CommissionAccrualEntity.DeveloperCompanyId; вход подбора правила |
CommissionRecipientUserId | Guid | Да | Получатель комиссии = участник-исполнитель (Executor) сделки на момент завершения (назначается руководителем, ADR-0071) → CommissionAccrualEntity.AgentUserId. Guid.Empty → комиссионное начисление не создаётся (skip, шаг 2; реферальная ветка выполняется); участник guard-а самореферала (INV-RA-4) |
OriginAgencyCompanyId | Guid | Да | → CommissionAccrualEntity.TenantId (tenant начисления = агентство-основатель сделки, решение №2 README) + снапшот атрибуции начисления (агентство-воронка) |
OriginAgentUserId | Guid | Да | → снапшот атрибуции начисления: иммутабельный агент-основатель сделки (историческая атрибуция / воронка агентства / антифрод). Не участвует в расчёте суммы и не является получателем |
PrimaryInvestorUserId | Guid | Да | → CommissionAccrualEntity.InvestorUserId (трассировка; в проекциях агентства маскируется) + ключ поиска активной реферальной связи (ReferredUserId реплики referral-bond.md) → ReferralAccrualEntity.InvestorUserId |
PropertyObjectId | Guid | Да | → CommissionAccrualEntity.PropertyObjectId |
ProjectId | Guid | Да | Вход scoped-резолюции правила застройщика / условий платформы / реферальной ставки (ось объекта Developer → Project → LayoutGroup, ADR-0068) + AppliedRuleSnapshot.PropertyObjectProjectId |
PropertyType | string (enum PropertyType: Apartment/Parking/Commercial) | Да | Вход предиката Conditions.PropertyTypes + AppliedRuleSnapshot.PropertyObjectPropertyType; unknown-значение — fail-closed для правил с непустым PropertyTypes (reward-rule.md) |
LayoutId | Guid? | Нет (парковка/коммерция без планировки) | Вход предиката Conditions.LayoutIds + AppliedRuleSnapshot.PropertyObjectLayoutId |
LayoutGroupId | Guid? | Нет | Вход предиката Conditions.LayoutGroupIds + AppliedRuleSnapshot.PropertyObjectLayoutGroupId |
Rooms | int? | Нет | Вход предиката RoomsRange (объект недвижимости без Rooms не проходит правило с заданным диапазоном) + AppliedRuleSnapshot.PropertyObjectRooms |
AreaTotalM2 | decimal | Да | Вход предиката AreaRangeM2 + AppliedRuleSnapshot.PropertyObjectAreaTotalM2 |
BasePrice.Amount / BasePrice.CurrencyId | decimal / short | Да | → CommissionAccrualEntity.BasePriceAmount (Money.Value/Money.CurrencyId); база расчёта при CalculationBase = BasePrice |
FinalPrice.Amount / FinalPrice.CurrencyId | decimal / short | Да | → CommissionAccrualEntity.FinalPriceAmount; база расчёта при CalculationBase = FinalPrice; база реферального начисления (ReferralAccrualEntity.BaseAmount, INV-RA-2) |
CompletedAt | DateTimeOffset | Да | → CommissionAccrualEntity.DealCompletedAt / ReferralAccrualEntity.DealCompletedAt; момент asOf для подбора правила застройщика, резолюции условий платформы и ставки реферального вознаграждения |
Поле ContractType payload-а consumer-ом не читается (не участвует ни в предикатах правил, ни в полях
начисления). ProjectId теперь читается — как измерение оси объекта в scoped-резолюции правила / условий
платформы / реферальной ставки (ADR-0068).
Алгоритм
- Контекст события. Восстановить метаданные события (идентификатор сообщения,
DealId, время); сверитьContractVersion— при несовпадении warn-and-proceed. Проверить идемпотентность по ключуrewards:crm-deal-completed:{MessageId}— дубль доставки →skipped-duplicate, пропуск (ack). - Проверка входа и идемпотентности.
- Формат/диапазоны:
DealId != Guid.Empty;TenantId != Guid.Empty;ProjectId != Guid.Empty;PropertyObjectId != Guid.Empty;PrimaryInvestorUserId != Guid.Empty;AreaTotalM2 > 0 && <= 10000;BasePrice.Amount >= 0,FinalPrice.Amount >= 0;CompletedAtзадан. Нарушение —REWARDS_DEAL_EVENT_INVALID(non-retriable → DLQ: контракт producer-а нарушен). - Согласованность получателя и агентства:
CommissionRecipientUserIdзадан, аOriginAgencyCompanyIdпуст →REWARDS_DEAL_EVENT_INVALID(non-retriable → DLQ + алерт — без tenant-агентства комиссионное начисление не размещается). - Валюта:
BasePrice.CurrencyId != FinalPrice.CurrencyId→REWARDS_CURRENCY_MISMATCH(non-retriable → DLQ; INV-6 commission-accrual.md; допустимость валюты — RUB-only, ADR-0048). - Сделка без исполнителя:
CommissionRecipientUserIdпуст (Guid.Empty) → комиссионная ветка (шаги 3–4) пропускается (assumption README): лог infoskipped-no-agent, метрикаrewards_accrual_skipped_no_agent_total; обработка продолжается реферальной веткой (шаг 5). - Бизнес-идемпотентность (INV-1): если комиссионное начисление по
DealIdуже существует → комиссионная ветка пропускается: логskipped-duplicate-deal, метрикаrewards_accrual_duplicate_deal_total. Ранее отменённое (Cancelled) начисление повторно не пересоздаётся — корректировка только ручным начислением застройщика (api/admin/create-manual-accrual.md). Реферальная ветка симметрично защищена INV-RA-1 (шаг 5) — повтор события пропускает обе ветки. - Unknown enum
PropertyType— не ошибка: значение трактуется как «неизвестный тип» (fail-closed для правил с непустымPropertyTypes, правило-default подходит) и логируется warningunknown-property-type.
- Формат/диапазоны:
- Что читаем (комиссионная ветка). Активные правила застройщика на момент
asOf = CompletedAt:TenantId = payload.TenantId,Status = Active, не удалённые, действующие по периоду (EffectiveFrom/EffectiveTo). Выборка берётся целиком (правил десятки, ADR-0056); из неё scoped-резолюцией выбирается наиболее специфичное активное правило по оси объектаDeveloper → Project → LayoutGroup(вход —ProjectId,LayoutGroupId?,LayoutId?события; более глубокое измерение каталога побеждает, при равной глубине —Priority; алгоритм — reward-rule.md, ADR-0068, INV гарантирует единственность). Условия платформы: наиболее специфичные действующие наasOf = CompletedAtпо агентской оси (OriginAgencyCompanyIdсделки) × оси объекта (Developer → Project → LayoutGroup), от узкого к широкому до глобального дефолта (platform-commission-terms.md, алгоритм резолюции). Политика агентства здесь не читается — она применяется позже, приConfirm(INV-4). - Комиссионная ветка: правило, условия платформы, начисление.
- Правило не найдено: комиссионное начисление не создаётся (решение №3 README) — warning-лог
no-rule-matchedсDealId/DeveloperCompanyId(без сумм), метрикаrewards_accrual_no_rule_total; обработка продолжается реферальной веткой (еёCommissionAccrualId = null— гейт денег непроходим до появления родителя). Застройщик может создать начисление вручную (api/admin/create-manual-accrual.md). - Расчёт суммы (INV-2): база =
BasePriceприCalculationBase = BasePrice, иначеFinalPrice;CommissionAmount = база × CommissionPercent / 100, округление banker's до 2 знаков. Если сумма<= 0(нулевая цена базы) → warningzero-commission, комиссионное начисление не создаётся (checkcommission_amount_value > 0); обработка продолжается реферальной веткой. - Резолюция условий платформы (asOf =
CompletedAt): агентские условия → глобальный дефолт → fallback 0 п.п. (PlatformRetentionPercentagePoints = 0,PlatformFeeAmount = 0,AppliedPlatformTermsId = null) + warn-метрикаrewards_platform_terms_missing_total+ алерт (платформа теряет доход). Guard INV-PT-4:RetentionPercentagePoints >= CommissionPercentприменённого правила (пул агентства ≤ 0) → событие целиком уходит в DLQREWARDS_PLATFORM_RETENTION_EXCEEDS_RULE— ни одно начисление (включая реферальное) не создаётся; после исправления условий или правила Менеджером ПИН событие переигрывается из DLQ. - Снапшот условий платформы (INV-10):
PlatformFeeAmount = база × RetentionPercentagePoints / 100(banker's, 2 знака; база — та же, что в INV-2); в начисление пишутсяPlatformRetentionPercentagePoints,PlatformFeeAmount,AppliedPlatformTermsId— последующая смена условий начисление не пересчитывает. - Snapshot правила: снимается
AppliedRuleSnapshotDocument(ContractVersion = 1) — id, имя и версия правила,CalculationBase,CommissionPercent,Priority, условия правила (MatchedConditions) и атрибуты объекта из события (PropertyObjectPropertyType,PropertyObjectProjectId,PropertyObjectLayoutId,PropertyObjectLayoutGroupId,PropertyObjectRooms,PropertyObjectAreaTotalM2) — для воспроизводимости scoped-расчёта (решение №4 README). - Снапшот атрибуции: из события снимаются
OriginAgentUserIdиOriginAgencyCompanyId(иммутабельная атрибуция агента-основателя и агентства-воронки) — историческая атрибуция / антифрод; на сумму и на получателя они не влияют (получатель —CommissionRecipientUserId). - Создание начисления: создаётся
CommissionAccrualEntityв статусеPendingConfirmation: tenant =OriginAgencyCompanyId(агентство-основатель),DealId,DeveloperCompanyId = payload.TenantId,AgentUserId = CommissionRecipientUserId(получатель = текущий Executor), снапшот атрибуции (OriginAgentUserId/OriginAgencyCompanyId),InvestorUserId = PrimaryInvestorUserId,PropertyObjectId,DealCompletedAt,CalculationBaseиCommissionPercentиз правила,BasePriceAmount,FinalPriceAmount,CommissionAmount, снапшот условий платформы,AppliedRuleIdи snapshot правила; guard-ы INV-2/INV-6/INV-10; порождается доменное событие начисления. Строки распределения на этом шаге не создаются (INV-4: только приConfirm; доляAgent— получателюCommissionRecipientUserId).
- Правило не найдено: комиссионное начисление не создаётся (решение №3 README) — warning-лог
- Реферальная ветка: активная связь, ставка, начисление (создание —
referral-accrual.md).
- Активная связь: по
PrimaryInvestorUserIdнайти связьStatus = Activeв репликеrewards.referral_bond_replicas(referral-bond.md). Связи нет → штатный случай (сделка без реферала): лог infoskipped-no-bond, реферальное начисление не создаётся. - Guard самореферала (INV-RA-4):
ReferrerUserIdсвязи совпадает с получателем комиссии (CommissionRecipientUserId) или агентом-основателем (OriginAgentUserId) сделки → начисление не создаётся: логskipped-self-referral, метрикаrewards_referral_self_skip_total(агенту-участнику сделки партнёрка за неё не платится; сделка без исполнителя проверку проходит поOriginAgentUserId). - Бизнес-идемпотентность (INV-RA-1): реферальное начисление по
DealIdуже существует → логskipped-duplicate-referral, ветка пропускается (существующее, включаяCancelled, не мутируется). - Резолюция ставки (asOf =
CompletedAt): scoped-резолюция наиболее специфичной ставки по партнёрской оси (ReferrerUserIdсвязи) × оси объектаDeveloper → Project → LayoutGroup(referral-rate-rule.md, ADR-0068); вход —ReferrerUserIdсвязи,DeveloperCompanyId = payload.TenantId,ProjectId,LayoutGroupId?события. Ставки нет → начисление не создаётся, warn-метрикаrewards_referral_accrual_no_rate_total. - Создание начисления:
Amount = FinalPrice × Percent / 100(banker's, 2 знака, INV-RA-2); снимаетсяAppliedRateSnapshot;CommissionAccrualId= id комиссионного начисления, созданного на шаге 4 этой же обработки (null, если комиссионная ветка завершилась skip-ом);ReferralAccrualEntityсоздаётся в статусеPending(PartnerUserId = ReferrerUserId,BondId,InvestorUserId = PrimaryInvestorUserId,DeveloperCompanyId,BaseAmount = FinalPrice,DealCompletedAt).
- Активная связь: по
- Что меняется. Появляются записи комиссионного и/или реферального начисления. Конкурентная доставка в
двух инстансах разрешается unique-индексами
ux_commission_accruals_deal_id/ux_referral_accruals_deal_id: нарушение уникальности при сохранении → перечитать начисление поDealIdи завершить ветку как идемпотентный дубль. - Какие факты/события фиксируются. По комиссионной ветке —
CommissionAccrualCreatedKafkaEvent(topicpin.rewards.commission-accrual-created.v1,ContractVersion = 1, key =Idначисления; контракт — ../events/commission-accrual-created.md); по реферальной —ReferralAccrualCreatedKafkaEvent(topicpin.rewards.referral-accruals.v1,ContractVersion = 1, key =Idначисления; контракт —../events/referral-accrual-created.md, по конвенции). Payload обоих собирается из полей агрегатов. - Согласованность. Оба начисления, отметка идемпотентности и исходящие события фиксируются в одном согласованном изменении; offset подтверждается после фиксации. В skip-ветках фиксируется только отметка идемпотентности (и эффект второй ветки, если она выполнилась).
- Результат. Успех → ack; non-retriable коды (
REWARDS_DEAL_EVENT_INVALID,REWARDS_CURRENCY_MISMATCH,REWARDS_PLATFORM_RETENTION_EXCEEDS_RULE) → DLQ + алерт; инфраструктурные сбои → retry (таблица ниже).
Диаграмма
Идемпотентность и replay
- Consumer-level: отметка по идентификатору сообщения (
rewards:crm-deal-completed:{MessageId}). - Business-level: business key =
DealIdв обеих ветках — проверка существующих начислений + unique indexux_commission_accruals_deal_id(INV-1) /ux_referral_accruals_deal_id(INV-RA-1); поведение дубля — skip ветки (существующее начисление не мутируется, включаяCancelled). - Replay: да. Полный re-read топика с offset 0 безопасен (каждая ветка с существующим начислением
пропускается). Replay полезен после исправления правил застройщика (
rewards_accrual_no_rule_total), реферальных ставок (rewards_referral_accrual_no_rate_total) или условий платформы (DLQREWARDS_PLATFORM_RETENTION_EXCEEDS_RULE) — но начисления создадутся по правилам/условиям/ставкам, активным наasOf = CompletedAt(ретроактивное исправление требует периода действия, покрывающего прошлое). Runbook —../runbooks/rewards-accrual-replay.md(по конвенции, README).
Retry и DLQ
DLQ-топик группы: pin.rewards.crm-deals-dlq.v1 (исходное событие + заголовки
x-dlq-reason, x-dlq-error-code, x-source-topic, x-original-offset).
| Сбой | Код | Retry | DLQ | Ручное действие |
|---|---|---|---|---|
| Инфраструктурный (PostgreSQL/Kafka недоступны, timeout) | — (exception) | Бесконечный exponential backoff 1s→60s с паузой партиции | Нет | Runbook rewards-overview: восстановить зависимость |
| Нарушение контракта payload (пустые обязательные поля, отрицательные цены, получатель без агентства-основателя) | REWARDS_DEAL_EVENT_INVALID | Нет (non-retriable) | Да + алерт | Разбор с командой crm; после фикса producer-а — replay из DLQ |
Разные валюты BasePrice/FinalPrice (CurrencyId) | REWARDS_CURRENCY_MISMATCH | Нет | Да + алерт | Разбор данных сделки в crm; при необходимости — ручное начисление |
| Удержание ПИН ≥ ставки применённого правила (INV-PT-4, пул ≤ 0) | REWARDS_PLATFORM_RETENTION_EXCEEDS_RULE | Нет | Да + алерт | Менеджер ПИН правит условия платформы или правило застройщика → replay из DLQ (создадутся оба начисления) |
Гонка unique DealId (две доставки параллельно, любая ветка) | — (нарушение уникальности) | 1 повтор: перечитать начисление по DealId → идемпотентный skip ветки | Нет | Не требуется |
| Правило не найдено | — (не ошибка) | — | Нет | Мониторинг rewards_accrual_no_rule_total; ручное начисление застройщиком |
| Условий платформы нет (fallback 0 п.п.) | — (не ошибка) | — | Нет | Мониторинг rewards_platform_terms_missing_total + алерт; Менеджер ПИН заводит условия |
| Реферальная ставка не найдена | — (не ошибка) | — | Нет | Мониторинг rewards_referral_accrual_no_rate_total; Менеджер ПИН заводит ставку → replay |
| Самореферал (реферер = агент сделки) | — (не ошибка) | — | Нет | Мониторинг rewards_referral_self_skip_total; разбор — admin-коррекции связей в referral |
Acceptance-критерии
- Happy path (оба начисления). Дано:
pin.crm.deal-completed.v1сCommissionRecipientUserId,OriginAgentUserId/OriginAgencyCompanyId,ProjectId, атрибутами объекта и обеими ценами; у застройщика есть подходящее активное правило (scoped на моментasOf); действуют условия платформы (например, 1.00 п.п. при ставке 10%); у инвестора активная реферальная связь и есть применимая ставка. Когда: consumer обрабатывает сообщение. Тогда: созданоCommissionAccrualEntity(PendingConfirmation, tenant =OriginAgencyCompanyId,AgentUserId = CommissionRecipientUserId, снапшот атрибуцииOriginAgentUserId/OriginAgencyCompanyId) со снапшотами правила и условий платформы (PlatformFeeAmount = база × 1.00 / 100, INV-10) иReferralAccrualEntity(Pending,CommissionAccrualId= id созданного начисления,Amount = FinalPrice × Percent / 100); опубликованыCommissionAccrualCreatedKafkaEventиReferralAccrualCreatedKafkaEvent; offset подтверждён. - Смена получателя (Executor ≠ основатель). Дано: исполнителя переназначал руководитель, поэтому
CommissionRecipientUserId≠OriginAgentUserId(и, возможно,OriginAgencyCompanyId). Когда: обработка. Тогда:CommissionAccrualEntity.AgentUserId = CommissionRecipientUserId(получатель = текущий Executor), доляAgentприConfirmпойдёт ему;OriginAgentUserId/OriginAgencyCompanyIdсохранены в снапшот атрибуции и на сумму/получателя не влияют. - Сделка без исполнителя. Дано:
CommissionRecipientUserIdпуст (Guid.Empty); у инвестора активная связь и ставка. Когда: обработка. Тогда: комиссионное начисление не создаётся (skipped-no-agent), реферальное создано (Pending,CommissionAccrualId = null— остаётсяPendingдо появления родителя), ack (не DLQ). - Правило не найдено. Дано: ни одно активное правило не подходит по
Conditions. Когда: обработка. Тогда: комиссионное начисление не создаётся,no-rule-matched, метрикаrewards_accrual_no_rule_total; реферальная ветка выполняется (CommissionAccrualId = null); ack; исправление — ручное начисление застройщика (привязывается к рефералке поDealId). - Условий платформы нет (fallback). Дано: правило найдено, но ни агентских, ни глобальных условий на
asOf = CompletedAtнет. Когда: обработка. Тогда: начисление создано сPlatformRetentionPercentagePoints = 0,PlatformFeeAmount = 0,AppliedPlatformTermsId = null; метрикаrewards_platform_terms_missing_total+ алерт; ack. - Удержание съедает ставку (INV-PT-4). Дано:
RetentionPercentagePoints >= CommissionPercentприменённого правила. Когда: обработка. Тогда: событие → DLQREWARDS_PLATFORM_RETENTION_EXCEEDS_RULE- алерт; ни комиссионное, ни реферальное начисление не созданы; после исправления — replay из DLQ.
- Нулевая комиссия. Дано: подходящее правило есть, но
CommissionAmount <= 0(нулевая база). Когда: обработка. Тогда: комиссионное начисление не создаётся,zero-commission; реферальная ветка выполняется; ack. - Реферал: активная связь и ставка. Дано: сделка приведённого инвестора; связь
Activeв реплике; применимая ставка уровняPartner. Когда: обработка. Тогда:ReferralAccrualEntityсоздано вPendingсо снапшотом ставки (Scope = Partner),BaseAmount = FinalPrice; опубликованоReferralAccrualCreatedKafkaEvent. - Реферал: связи нет. Дано: у инвестора нет активной связи. Когда: обработка. Тогда: реферальное
начисление не создаётся,
skipped-no-bond(штатно, без метрик ошибок), ack. - Реферал: самореферал (INV-RA-4). Дано:
ReferrerUserIdактивной связи = получатель комиссии (CommissionRecipientUserId) или агент-основатель (OriginAgentUserId) сделки. Когда: обработка. Тогда: реферальное начисление не создаётся,skipped-self-referral, метрикаrewards_referral_self_skip_total; комиссионная ветка не затронута. - Реферал: ставки нет. Дано: активная связь есть, применимой ставки на
asOfнет. Когда: обработка. Тогда: реферальное начисление не создаётся, метрикаrewards_referral_accrual_no_rate_total, ack. - Дубль сделки (INV-1/INV-RA-1). Дано: по
DealIdначисления уже существуют (в т.ч.Cancelled). Когда: повторная/replay-доставка. Тогда:skipped-duplicate-deal/skipped-duplicate-referral, существующие начисления не мутируются, ack. - Дубль доставки. Дано: то же
MessageIdдоставлено повторно. Когда: обработка. Тогда:skipped-duplicate, эффектов нет, ack. - Нарушение контракта. Дано: пустой обязательный id / отрицательная цена /
CommissionRecipientUserIdбезOriginAgencyCompanyId. Когда: обработка. Тогда:REWARDS_DEAL_EVENT_INVALID→ DLQ + алерт (non-retriable). - Несовпадение валют. Дано:
BasePrice.CurrencyId != FinalPrice.CurrencyId. Когда: обработка. Тогда:REWARDS_CURRENCY_MISMATCH→ DLQ + алерт. - Unknown PropertyType. Дано: неизвестное значение
PropertyType. Когда: обработка. Тогда: fail-closed (правила с непустымPropertyTypesне подходят), warningunknown-property-type, обработка продолжается. - Инфраструктурный сбой. Дано: PostgreSQL/Kafka недоступны. Когда: обработка. Тогда: retry с backoff (offset не двигается), без DLQ.
Наблюдаемость и безопасность
- Logs (structured):
consumed/skipped-duplicate/skipped-duplicate-deal/skipped-duplicate-referral/skipped-no-agent/no-rule-matched/zero-commission/skipped-no-bond/skipped-self-referral/accrual-created/referral-accrual-created/dead-letteredс полямиMessageId,DealId,DeveloperCompanyId,OriginAgencyCompanyId,CommissionRecipientUserId,OriginAgentUserId,AppliedRuleId,AppliedPlatformTermsId,BondId. Суммы комиссий, цены и пункты удержания в логи не пишутся (Sensitive-класс, README «Безопасность»);PrimaryInvestorUserIdв логи не пишется. - Metrics:
rewards_deal_completed_consume_duration_ms,rewards_accrual_created_total,rewards_accrual_no_rule_total(алерт README),rewards_accrual_skipped_no_agent_total,rewards_accrual_duplicate_deal_total,rewards_platform_terms_missing_total(алерт — платформа теряет доход),rewards_referral_accrual_created_total,rewards_referral_accrual_no_rate_total,rewards_referral_self_skip_total,rewards_crm_deals_lag_seconds,rewards_crm_deals_dlq_total,rewards_contract_mismatch_total. - Traces: span приёма события → span обработки → spans обращений к хранилищу; корреляция из метаданных события пробрасывается в исходящее событие.
- Alerts: lag consumer-group > 5 мин (SLO README: < 1 мин штатно); DLQ > 0; рост
rewards_accrual_no_rule_total. - Dashboard / Runbook: Grafana
rewards-overview; runbookrewards-accrual-replay.md. - Безопасность: system-actor (не пользовательский контекст), platform-scope (вне tenant-контекста) с
явной установкой
TenantId = OriginAgencyCompanyIdиз payload; tenant mismatch невозможен по построению (все ссылки — из payload доверенного внутреннего топикаcrm); payload не содержит untrusted-пользовательского ввода, кроме уже валидированныхcrmполей.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (5): Entity / Aggregate: CommissionAccrualEntity, Entity / Aggregate: PlatformCommissionTermsEntity, Entity / Aggregate: ReferralAccrualEntity, Entity / Aggregate: ReferralRateRuleEntity, Entity / Projection: AgentPerformanceEntity
- API (10): GET /api/v1/agency/commission-accruals — Список начислений агентства, GET /api/v1/my/commission-summary — Моя сводка комиссий (агент), POST /api/v1/developer/commission-accruals — Ручное создание начисления застройщиком, POST /api/v1/platform/commission-terms — Создание условий платформенной комиссии (черновик), POST /api/v1/platform/commission-terms/{termsId}/activate — Активация условий платформенной комиссии, POST /api/v1/platform/commission-terms/{termsId}/archive — Архивирование условий платформенной комиссии, POST /api/v1/platform/referral-rate-rules — Создание ставки реферального вознаграждения (черновик), POST /api/v1/platform/referral-rate-rules/{rateRuleId}/activate — Активация ставки реферального вознаграждения, POST /api/v1/reward-rules/test-calculation — Тестовый расчёт комиссии по атрибутам объекта недвижимости, POST /internal/v1/commission/calculate-property-object — Расчёт комиссии по объекту недвижимости (internal, s2s для catalog)
- Use Cases (4): UC-RWD-003. Сделка завершена — начисление комиссии (system), UC-RWD-007. Платформа настраивает условия удержания ПИН, UC-RWD-008. Платформа настраивает ставки реферального вознаграждения, UC-RWD-009. Жизненный цикл реферального начисления (связь → начисление → гейт → выплата)
- События (4): Event Contract: pin.crm.deal-completed (DealCompletedEvent), Event Contract: pin.crm.fixation-approved (FixationApprovedEvent), Event Contract: pin.rewards.commission-accrual-created, Event Contract: pin.rewards.referral-accrual-created (ReferralAccrualCreatedKafkaEvent)
- Консюмеры (5): Consumer: Agent Performance Projection, Consumer: Chessboard Dimensions Sync (pin-rewards-chessboard-dimensions), Consumer: CRM Deal Cancelled, Consumer: CRM Fixation Approved, Consumer: Referral Bond Sync (pin-rewards-referral-bonds)
- Саги (2): Saga «Commission Settlement» (финансовый контур вознаграждений), Saga «Deal Completion» (fan-out завершения сделки)
- index.md (1): Rewards Service
Связанные документы
- Сущности: commission-accrual.md (дом полей, INV-1/2/6/10), referral-accrual.md (реферальное начисление, INV-RA-1..4), reward-rule.md (алгоритм подбора, семантика условий), platform-commission-terms.md (резолюция условий, INV-PT-4), referral-rate-rule.md (резолюция ставки).
- Событие-источник: ../../crm/events/deal-completed.md; сделка — deal.md. Snapshot-модели валюты — ../../handbook/domain/snapshots.md.
- Производимые события: commission-accrual-created.md;
../events/referral-accrual-created.md(по конвенции, топикpin.rewards.referral-accruals.v1). - Соседние consumers: crm-deal-cancelled.md (отмена начислений,
CancelReason = DealCancelled), referral-bond.md (реплика связей). - Сквозной процесс: commission-settlement-saga.md — хореография финансового контура (deal-completed → начисления → подтверждение → акт → выплаты).
- Шаблон: consumer-template.md; ADR-0052 (tenancy), ADR-0056
(simplicity-first — подбор правил/условий/ставок выборкой в память),
ADR-0066
(удержание ПИН, реферальные ставки),
ADR-0067 (гейт получения денег),
ADR-0068
(scoped + time-bounded резолюция правил/условий/ставок по оси
Developer → Project → LayoutGroup), ADR-0071 (получатель комиссии = Executor на момент завершения;Origin*— иммутабельная атрибуция).