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

Consumer: CRM Deal Completed

Назначение

Consumer слушает событие завершения сделки CRM (Sale → Completed, deal.md) и запускает финансовый контур вознаграждений — два независимых эффекта одного события:

  • Начисление комиссии агенту/агентству: подбирает правило вознаграждения застройщика по атрибутам объекта недвижимости, считает gross CommissionAmount = база × CommissionPercent / 100, резолвит условия платформы на момент завершения сделки и снимает снапшот удержания ПИН (PlatformRetentionPercentagePoints / PlatformFeeAmount / AppliedPlatformTermsIdplatform-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
Topicpin.crm.deal-completed.v1
Partitions / keykey = DealId — строгий порядок per-deal
ContractVersion1; при несовпадении версии — warn-and-proceed (метрика rewards_contract_mismatch_total)
Consumer grouppin-rewards-crm-deals (общая с consumer-ом отмены — crm-deal-cancelled.md, документ по конвенции)
Producercrm (переход 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ТипОбязательностьКак используется (маппинг)
DealIdGuidДаБизнес-ключ идемпотентности → CommissionAccrualEntity.DealId (unique)
TenantIdGuidДа= DeveloperCompanyId сделки (crm tenant = застройщик) → CommissionAccrualEntity.DeveloperCompanyId; вход подбора правила
CommissionRecipientUserIdGuidДаПолучатель комиссии = участник-исполнитель (Executor) сделки на момент завершения (назначается руководителем, ADR-0071) → CommissionAccrualEntity.AgentUserId. Guid.Empty → комиссионное начисление не создаётся (skip, шаг 2; реферальная ветка выполняется); участник guard-а самореферала (INV-RA-4)
OriginAgencyCompanyIdGuidДаCommissionAccrualEntity.TenantId (tenant начисления = агентство-основатель сделки, решение №2 README) + снапшот атрибуции начисления (агентство-воронка)
OriginAgentUserIdGuidДа→ снапшот атрибуции начисления: иммутабельный агент-основатель сделки (историческая атрибуция / воронка агентства / антифрод). Не участвует в расчёте суммы и не является получателем
PrimaryInvestorUserIdGuidДаCommissionAccrualEntity.InvestorUserId (трассировка; в проекциях агентства маскируется) + ключ поиска активной реферальной связи (ReferredUserId реплики referral-bond.md) → ReferralAccrualEntity.InvestorUserId
PropertyObjectIdGuidДаCommissionAccrualEntity.PropertyObjectId
ProjectIdGuidДаВход scoped-резолюции правила застройщика / условий платформы / реферальной ставки (ось объекта Developer → Project → LayoutGroup, ADR-0068) + AppliedRuleSnapshot.PropertyObjectProjectId
PropertyTypestring (enum PropertyType: Apartment/Parking/Commercial)ДаВход предиката Conditions.PropertyTypes + AppliedRuleSnapshot.PropertyObjectPropertyType; unknown-значение — fail-closed для правил с непустым PropertyTypes (reward-rule.md)
LayoutIdGuid?Нет (парковка/коммерция без планировки)Вход предиката Conditions.LayoutIds + AppliedRuleSnapshot.PropertyObjectLayoutId
LayoutGroupIdGuid?НетВход предиката Conditions.LayoutGroupIds + AppliedRuleSnapshot.PropertyObjectLayoutGroupId
Roomsint?НетВход предиката RoomsRange (объект недвижимости без Rooms не проходит правило с заданным диапазоном) + AppliedRuleSnapshot.PropertyObjectRooms
AreaTotalM2decimalДаВход предиката AreaRangeM2 + AppliedRuleSnapshot.PropertyObjectAreaTotalM2
BasePrice.Amount / BasePrice.CurrencyIddecimal / shortДаCommissionAccrualEntity.BasePriceAmount (Money.Value/Money.CurrencyId); база расчёта при CalculationBase = BasePrice
FinalPrice.Amount / FinalPrice.CurrencyIddecimal / shortДаCommissionAccrualEntity.FinalPriceAmount; база расчёта при CalculationBase = FinalPrice; база реферального начисления (ReferralAccrualEntity.BaseAmount, INV-RA-2)
CompletedAtDateTimeOffsetДаCommissionAccrualEntity.DealCompletedAt / ReferralAccrualEntity.DealCompletedAt; момент asOf для подбора правила застройщика, резолюции условий платформы и ставки реферального вознаграждения

Поле ContractType payload-а consumer-ом не читается (не участвует ни в предикатах правил, ни в полях начисления). ProjectId теперь читается — как измерение оси объекта в scoped-резолюции правила / условий платформы / реферальной ставки (ADR-0068).

Алгоритм

  1. Контекст события. Восстановить метаданные события (идентификатор сообщения, DealId, время); сверить ContractVersion — при несовпадении warn-and-proceed. Проверить идемпотентность по ключу rewards:crm-deal-completed:{MessageId} — дубль доставки → skipped-duplicate, пропуск (ack).
  2. Проверка входа и идемпотентности.
    • Формат/диапазоны: 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.CurrencyIdREWARDS_CURRENCY_MISMATCH (non-retriable → DLQ; INV-6 commission-accrual.md; допустимость валюты — RUB-only, ADR-0048).
    • Сделка без исполнителя: CommissionRecipientUserId пуст (Guid.Empty) → комиссионная ветка (шаги 3–4) пропускается (assumption README): лог info skipped-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 подходит) и логируется warning unknown-property-type.
  3. Что читаем (комиссионная ветка). Активные правила застройщика на момент 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).
  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 (нулевая цена базы) → warning zero-commission, комиссионное начисление не создаётся (check commission_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) → событие целиком уходит в DLQ REWARDS_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).
  5. Реферальная ветка: активная связь, ставка, начисление (создание — referral-accrual.md).
    • Активная связь: по PrimaryInvestorUserId найти связь Status = Active в реплике rewards.referral_bond_replicas (referral-bond.md). Связи нет → штатный случай (сделка без реферала): лог info skipped-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).
  6. Что меняется. Появляются записи комиссионного и/или реферального начисления. Конкурентная доставка в двух инстансах разрешается unique-индексами ux_commission_accruals_deal_id / ux_referral_accruals_deal_id: нарушение уникальности при сохранении → перечитать начисление по DealId и завершить ветку как идемпотентный дубль.
  7. Какие факты/события фиксируются. По комиссионной ветке — CommissionAccrualCreatedKafkaEvent (topic pin.rewards.commission-accrual-created.v1, ContractVersion = 1, key = Id начисления; контракт — ../events/commission-accrual-created.md); по реферальной — ReferralAccrualCreatedKafkaEvent (topic pin.rewards.referral-accruals.v1, ContractVersion = 1, key = Id начисления; контракт — ../events/referral-accrual-created.md, по конвенции). Payload обоих собирается из полей агрегатов.
  8. Согласованность. Оба начисления, отметка идемпотентности и исходящие события фиксируются в одном согласованном изменении; offset подтверждается после фиксации. В skip-ветках фиксируется только отметка идемпотентности (и эффект второй ветки, если она выполнилась).
  9. Результат. Успех → 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 index ux_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) или условий платформы (DLQ REWARDS_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).

СбойКодRetryDLQРучное действие
Инфраструктурный (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 ≠ основатель). Дано: исполнителя переназначал руководитель, поэтому CommissionRecipientUserIdOriginAgentUserId (и, возможно, 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 применённого правила. Когда: обработка. Тогда: событие → DLQ REWARDS_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 не подходят), warning unknown-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; runbook rewards-accrual-replay.md.
  • Безопасность: system-actor (не пользовательский контекст), platform-scope (вне tenant-контекста) с явной установкой TenantId = OriginAgencyCompanyId из payload; tenant mismatch невозможен по построению (все ссылки — из payload доверенного внутреннего топика crm); payload не содержит untrusted-пользовательского ввода, кроме уже валидированных crm полей.

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

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

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

  • Сущности: 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* — иммутабельная атрибуция).