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

UC-RWD-002. Агент видит комиссию на объекте недвижимости в каталоге

Назначение

Описывает сценарий отображения агенту комиссии «сколько я заработаю на этом объекте недвижимости» в публичном каталоге: catalog запрашивает у rewards внутренним s2s-API применимое правило вознаграждения, gross-комиссию и чистую экономику агентской стороны — удержание платформы по условиям ПИН (ADR-0066) и пул агентства (gross − удержание), net-цифры для блока комиссии. Read-only сценарий. Связан с контекстом rewards.

Metadata

ПолеЗначение
IDUC-RWD-002
Контекстrewards (см. docs/06-services/rewards/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАгент (конечный пользователь UI); фактический вызывающий rewards — сервис catalog (s2s, system-actor)
Permissions / scopeUI-гейт в catalog: rewards.commission.view-property-object (агенты/менеджеры; инвестору и гостю блок комиссии не отдаётся); s2s-канал catalog → rewards — internal service credentials (mTLS/JWT сервисного клиента), пользовательский permission прокидывается catalog-ом как признак «показывать ли блок»
TenantПравила читаются по явному DeveloperCompanyId объекта недвижимости (не по tenant актора — агент принадлежит агентству, правила — застройщику); tenant-интерсептор на internal query-path не применяется, фильтр по TenantId = DeveloperCompanyId задаётся явно
Record-levelне применяется (расчёт не привязан к конкретному агенту — правило застройщика едино для всех агентов)

Предусловие

  • У застройщика объекта недвижимости есть хотя бы одно правило Status = Active в актуальном периоде (UC-RWD-001) — иначе поток A1 (блок комиссии скрывается).
  • catalog имеет атрибуты объекта недвижимости в своей денормализованной витрине: DeveloperCompanyId, PropertyType, LayoutId?, LayoutGroupId?, Rooms?, AreaTotalM2, BasePrice, FinalPrice (реплики из chessboard).
  • Пользователь аутентифицирован как агент/менеджер с rewards.commission.view-property-object.

Триггер

ПолеЗначение
ИсточникUI (открытие карточки объекта недвижимости) → s2s API call
Триггерящий элементКаталог → карточка объекта недвижимости → блок «Ваша комиссия»; список объектов недвижимости проекта (шахматка-витрина) — из кэша витрины catalog

Основной Поток

  1. Агент открывает карточку объекта недвижимости в каталоге. Фронт запрашивает карточку у catalog; catalog видит, что актор — агент с rewards.commission.view-property-object, и включает в ответ блок комиссии.
  2. catalog вызывает внутренний endpoint POST /internal/v1/commission/calculate-property-object (s2s, ../api/internal/calculate-property-object-commission.md) с телом { developerCompanyId: Guid, agencyCompanyId?: Guid (агентство смотрящего агента — для per-agency условий платформы), propertyType: PropertyType (Apartment | Parking | Commercial), layoutId?: Guid, layoutGroupId?: Guid, rooms?: int (0..20), areaTotalM2: decimal (> 0, scale 2), basePrice: MoneyModel { value, currencyId }, finalPrice: MoneyModel, asOf?: DateTimeOffset (default now) }. POST — потому что параметры не помещаются в query без потери типизации; операция идемпотентна и read-only.
  3. Система рассчитывает комиссию (read-only):
    • Проверка входа. developerCompanyId != Guid.Empty; areaTotalM2 > 0; basePrice.currencyId == finalPrice.currencyId (иначе REWARDS_CURRENCY_MISMATCH; допустимость валюты — RUB-only, ADR-0048). Неизвестное значение propertyType (новый тип в chessboard) допускается — обрабатывается fail-closed семантикой условий (reward-rule.md, enum PropertyType). Существование застройщика не проверяется (нет правил → HasRule = false, A1).
    • Выборка правил. Активные правила застройщика на момент asOf: TenantId = developerCompanyId, Status = Active, не архивированы, EffectiveFrom не в будущем и EffectiveTo не истёк (десятки правил на застройщика, ADR-0056; индекс ix_reward_rules_tenant_status).
    • Подбор и расчёт (ResolveRule). По предикатам Conditions (семантика И между измерениями; пустое измерение = any; объект недвижимости без rooms не проходит правило с заданным диапазоном комнат; unknown propertyType не проходит правило с непустым PropertyTypes) из подошедших берётся единственное с максимальным Priority (уникальность — INV-3). Сумма: commissionBase = rule.CalculationBase == BasePrice ? basePrice : finalPrice; commissionAmount = round(commissionBase.value × rule.CommissionPercent / 100, 2, banker's) (формула — INV-2 commission-accrual.md).
    • Удержание платформы и пул (net). Резолюция условий платформы на asOf — тот же алгоритм, что при создании начисления (platform-commission-terms.md, UC-RWD-007): условия агентства agencyCompanyId → глобальные → fallback 0 п.п.; platformFeeAmount = commissionBase × retentionPP / 100 (banker's, 2 знака, формула INV-10), agencyPoolAmount = commissionAmount − platformFeeAmount — net-сумма агентской стороны (деление пула агент/агентство — политика агентства, витрине не отдаётся).
    • Результат. { hasRule, resolvedAsOf, appliedRule?: { appliedRuleId, ruleName, calculationBase, commissionPercent, priority, commissionBase, commissionAmount, platformRetentionPercentagePoints, platformFeeAmount, agencyPoolAmount } }; правило не найдено → 200 с hasRule = false (A1). Маскирование не требуется — процент и суммы предназначены агенту, PII нет.
  4. catalog кэширует результат в своей денормализованной витрине (инвалидация — consumer pin.rewards.reward-rule-changed.v1, поток A3) и отдаёт фронту карточку объекта недвижимости с блоком «Ваша комиссия»: пул агентской стороны (net после удержания ПИН) и ставка «X% от базовой/конечной цены».
  5. Фронт рендерит блок комиссии в карточке объекта недвижимости; списки (шахматка-витрина) catalog обслуживает из своей денормализованной витрины по тем же кэшированным расчётам.

Альтернативные Потоки

A1. Правило не найдено

  1. ResolveRule не нашёл подошедших активных правил (нет правил, объект недвижимости не попал ни в один предикат, период истёк) → 200 с hasRule = false.
  2. catalog не рендерит блок комиссии (без ошибки). Начисление по такой сделке тоже не создастся автоматически (UC-RWD-003, A3). Завершается.

A2. Актор без права видеть комиссию

  1. Актор — инвестор/гость или агент без rewards.commission.view-property-object: catalog не вызывает rewards и не включает блок комиссии в ответ (permission-гейт на стороне catalog; инвестору комиссия не отдаётся — правило безопасности reward-rule.md, раздел «Индексы, хранение, безопасность»). Завершается.

A3. Инвалидация кэша каталога по изменению правила

  1. Менеджер застройщика активирует/деактивирует/архивирует правило или меняет EffectiveTo (UC-RWD-001) → pin.rewards.reward-rule-changed.v1.
  2. Consumer в catalog (../../catalog/consumers/ — по конвенции) инвалидирует кэшированную комиссию объектов недвижимости застройщика TenantId события; при следующем открытии карточки — повторный расчёт (шаги 2–4).

A4. rewards недоступен

  1. Вызов s2s завершился ошибкой/таймаутом → catalog применяет graceful degradation: карточка объекта недвижимости отдаётся без блока комиссии (недоступность rewards не блокирует каталог — README «Зависимости»). catalog логирует warning с correlation id; retry — на следующем запросе.

A5. Валюты цен различаются

  1. basePrice.currencyId != finalPrice.currencyIdREWARDS_CURRENCY_MISMATCH (400). Указывает на дефект данных витрины catalog; блок комиссии скрывается, ошибка логируется.

A6. Аномалия конфигурации: пул ≤ 0

  1. Удержание платформы ≥ CommissionPercent применённого правила (INV-PT-4) → расчёт отдаёт цифры как есть (agencyPoolAmount <= 0) + warn-метрика; catalog обязан скрывать net-блок при agencyPoolAmount <= 0 (fail-closed отображения). Фактическое начисление по такой сделке не создастся до исправления условий (UC-RWD-003 A7, DLQ REWARDS_PLATFORM_RETENTION_EXCEEDS_RULE).

Постусловие

  • Состояние rewards не изменилось (read-only); события не публиковались.
  • Агент видит в карточке объекта недвижимости net-экономику: пул агентской стороны (gross − удержание ПИН) и процент, посчитанные по единственному детерминированно выбранному правилу застройщика и тем же условиям платформы, которые применятся при завершении сделки (UC-RWD-003) на тех же атрибутах, агентстве и asOf — согласованность превью и фактического начисления.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента (catalog)
ValidationError400Нарушение формата формы (areaTotalM2 <= 0, пустой developerCompanyId)Скрыть блок комиссии, залогировать дефект данных витрины
REWARDS_CURRENCY_MISMATCH400currencyId цен basePrice/finalPrice различаются (A5)Скрыть блок, алерт на данные
401/403401/403Невалидные s2s-credentials клиента catalogСкрыть блок, алерт конфигурации
hasRule = false200Правило не найдено (A1) — не ошибкаНе рендерить блок комиссии
5xx / timeout5xxrewards недоступен (A4)Graceful degradation: карточка без блока комиссии

Kafka Интеграционные События

Сам сценарий событий не публикует (read-only). Входящее для catalog (контекст A3):

СобытиеTopicКогда публикуетсяConsumers
RewardRuleChangedKafkaEvent.v1pin.rewards.reward-rule-changed.v1Изменение правил застройщиком (UC-RWD-001)catalog — инвалидация кэша комиссий

Acceptance Criteria

#КритерийПроверка
AC1Для объекта недвижимости, подходящего нескольким правилам, возвращается правило с максимальным Priorityтест: два пересекающихся правила → выбран больший приоритет
AC2Сумма считается от базы, заданной правилом: BasePrice — до ремонта/скидки, FinalPrice — базовая + ремонтпараметризованный тест обеих баз, banker's rounding до 2 знаков
AC3Объект недвижимости без rooms (парковка) не матчится правилом с диапазоном комнат; матчится правилом-defaultunit-тест предикатов Conditions
AC4Правило вне периода EffectiveFrom/EffectiveTo не участвует в подборетест с asOf вне периода → правило пропущено
AC5Нет подошедших правил → hasRule = false, каталог скрывает блок без ошибки (A1)интеграционный тест пустого результата
AC6Инвестору/гостю блок комиссии не отдаётся (A2)тест catalog-гейта: инвестор → нет блока
AC7Расчёт в каталоге (gross, удержание ПИН, пул) совпадает с цифрами будущего начисления при неизменных правилах и условияхсквозной тест UC-RWD-002 → UC-RWD-003 на одном объекте недвижимости и агентстве
AC8Net-цифры: platformFeeAmount = база × п.п. / 100, agencyPoolAmount = gross − удержание; per-agency условия побеждают глобальные; без условий пул = gross (A6/UC-RWD-007)параметризованный тест резолюции условий
AC9Недоступность rewards не блокирует карточку каталога (A4)s2s timeout → карточка отдаётся без блока комиссии, warning в логе
AC10При agencyPoolAmount <= 0 (аномалия INV-PT-4) catalog скрывает net-блок (A6)тест fail-closed отображения

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

  • Латентность. Горячий путь (карточка/список открывается на каждый просмотр агента): подбор идёт по выборке десятков активных правил застройщика и единиц условий платформы в памяти (ADR-0056), внешних вызовов нет — целевой p95 единичного расчёта — единицы/десятки мс (SLO < 100 мс).
  • Метрики (бизнес-смысл): доля запросов с null-результатом (нет правила) — сигнал, что застройщик не настроил вознаграждения; счётчик REWARDS_CURRENCY_MISMATCH — индикатор дефекта данных витрины; доля graceful-degradation (A4) — здоровье s2s-канала catalog → rewards.
  • Безопасность логов. Суммы и проценты комиссии — Sensitive: в логи rewards не пишутся; корреляция — сквозной trace от catalog. Инвестору/гостю блок не рассчитывается вовсе (гейт на стороне catalog).

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

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

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

  • API метод: ../api/internal/calculate-property-object-commission.md (контракт запроса/ответа, AppliedPropertyObjectCommissionModel).
  • Domain entities: ../domain/reward-rule.md (алгоритм ResolveRule, семантика Conditions, fail-closed для unknown PropertyType), ../domain/platform-commission-terms.md (резолюция удержания ПИН — net-цифры).
  • Формулы сумм: ../domain/commission-accrual.md (INV-2, INV-10).
  • Потребитель: ../../catalog/README.md — витрина, кэш и consumer reward-rule-changed (../../catalog/consumers/ — будущий путь по конвенции).
  • Связанные UC: UC-RWD-001 (откуда берутся правила), UC-RWD-007 (условия платформы), UC-RWD-003 (те же формулы при начислении).