UC-RWD-002. Агент видит комиссию на объекте недвижимости в каталоге
Назначение
Описывает сценарий отображения агенту комиссии «сколько я заработаю на этом объекте недвижимости» в публичном каталоге:
catalog запрашивает у rewards внутренним s2s-API применимое правило вознаграждения, gross-комиссию и
чистую экономику агентской стороны — удержание платформы по условиям ПИН (ADR-0066) и пул агентства
(gross − удержание), net-цифры для блока комиссии. Read-only сценарий. Связан с контекстом rewards.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-RWD-002 |
| Контекст | rewards (см. docs/06-services/rewards/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Агент (конечный пользователь UI); фактический вызывающий rewards — сервис catalog (s2s, system-actor) |
| Permissions / scope | UI-гейт в 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 |
Основной Поток
- Агент открывает карточку объекта недвижимости в каталоге. Фронт запрашивает карточку у
catalog;catalogвидит, что актор — агент сrewards.commission.view-property-object, и включает в ответ блок комиссии. catalogвызывает внутренний endpointPOST /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.- Система рассчитывает комиссию (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, enumPropertyType). Существование застройщика не проверяется (нет правил →HasRule = false, A1). - Выборка правил. Активные правила застройщика на момент
asOf:TenantId = developerCompanyId,Status = Active, не архивированы,EffectiveFromне в будущем иEffectiveToне истёк (десятки правил на застройщика, ADR-0056; индексix_reward_rules_tenant_status). - Подбор и расчёт (
ResolveRule). По предикатамConditions(семантика И между измерениями; пустое измерение = any; объект недвижимости безroomsне проходит правило с заданным диапазоном комнат; unknownpropertyTypeне проходит правило с непустым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 нет.
- Проверка входа.
catalogкэширует результат в своей денормализованной витрине (инвалидация — consumerpin.rewards.reward-rule-changed.v1, поток A3) и отдаёт фронту карточку объекта недвижимости с блоком «Ваша комиссия»: пул агентской стороны (net после удержания ПИН) и ставка «X% от базовой/конечной цены».- Фронт рендерит блок комиссии в карточке объекта недвижимости; списки (шахматка-витрина)
catalogобслуживает из своей денормализованной витрины по тем же кэшированным расчётам.
Альтернативные Потоки
A1. Правило не найдено
ResolveRuleне нашёл подошедших активных правил (нет правил, объект недвижимости не попал ни в один предикат, период истёк) →200сhasRule = false.catalogне рендерит блок комиссии (без ошибки). Начисление по такой сделке тоже не создастся автоматически (UC-RWD-003, A3). Завершается.
A2. Актор без права видеть комиссию
- Актор — инвестор/гость или агент без
rewards.commission.view-property-object:catalogне вызываетrewardsи не включает блок комиссии в ответ (permission-гейт на сторонеcatalog; инвестору комиссия не отдаётся — правило безопасности reward-rule.md, раздел «Индексы, хранение, безопасность»). Завершается.
A3. Инвалидация кэша каталога по изменению правила
- Менеджер застройщика активирует/деактивирует/архивирует правило или меняет
EffectiveTo(UC-RWD-001) →pin.rewards.reward-rule-changed.v1. - Consumer в
catalog(../../catalog/consumers/— по конвенции) инвалидирует кэшированную комиссию объектов недвижимости застройщикаTenantIdсобытия; при следующем открытии карточки — повторный расчёт (шаги 2–4).
A4. rewards недоступен
- Вызов s2s завершился ошибкой/таймаутом →
catalogприменяет graceful degradation: карточка объекта недвижимости отдаётся без блока комиссии (недоступностьrewardsне блокирует каталог — README «Зависимости»).catalogлогирует warning с correlation id; retry — на следующем запросе.
A5. Валюты цен различаются
basePrice.currencyId != finalPrice.currencyId→REWARDS_CURRENCY_MISMATCH(400). Указывает на дефект данных витриныcatalog; блок комиссии скрывается, ошибка логируется.
A6. Аномалия конфигурации: пул ≤ 0
- Удержание платформы ≥
CommissionPercentприменённого правила (INV-PT-4) → расчёт отдаёт цифры как есть (agencyPoolAmount <= 0) + warn-метрика;catalogобязан скрывать net-блок приagencyPoolAmount <= 0(fail-closed отображения). Фактическое начисление по такой сделке не создастся до исправления условий (UC-RWD-003 A7, DLQREWARDS_PLATFORM_RETENTION_EXCEEDS_RULE).
Постусловие
- Состояние
rewardsне изменилось (read-only); события не публиковались. - Агент видит в карточке объекта недвижимости net-экономику: пул агентской стороны (gross − удержание
ПИН) и процент, посчитанные по единственному детерминированно выбранному правилу застройщика и тем же
условиям платформы, которые применятся при завершении сделки (UC-RWD-003) на тех же атрибутах,
агентстве и
asOf— согласованность превью и фактического начисления.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента (catalog) |
|---|---|---|---|
ValidationError | 400 | Нарушение формата формы (areaTotalM2 <= 0, пустой developerCompanyId) | Скрыть блок комиссии, залогировать дефект данных витрины |
REWARDS_CURRENCY_MISMATCH | 400 | currencyId цен basePrice/finalPrice различаются (A5) | Скрыть блок, алерт на данные |
401/403 | 401/403 | Невалидные s2s-credentials клиента catalog | Скрыть блок, алерт конфигурации |
hasRule = false | 200 | Правило не найдено (A1) — не ошибка | Не рендерить блок комиссии |
5xx / timeout | 5xx | rewards недоступен (A4) | Graceful degradation: карточка без блока комиссии |
Kafka Интеграционные События
Сам сценарий событий не публикует (read-only). Входящее для catalog (контекст A3):
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
RewardRuleChangedKafkaEvent.v1 | pin.rewards.reward-rule-changed.v1 | Изменение правил застройщиком (UC-RWD-001) | catalog — инвалидация кэша комиссий |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Для объекта недвижимости, подходящего нескольким правилам, возвращается правило с максимальным Priority | тест: два пересекающихся правила → выбран больший приоритет |
| AC2 | Сумма считается от базы, заданной правилом: BasePrice — до ремонта/скидки, FinalPrice — базовая + ремонт | параметризованный тест обеих баз, banker's rounding до 2 знаков |
| AC3 | Объект недвижимости без rooms (парковка) не матчится правилом с диапазоном комнат; матчится правилом-default | unit-тест предикатов Conditions |
| AC4 | Правило вне периода EffectiveFrom/EffectiveTo не участвует в подборе | тест с asOf вне периода → правило пропущено |
| AC5 | Нет подошедших правил → hasRule = false, каталог скрывает блок без ошибки (A1) | интеграционный тест пустого результата |
| AC6 | Инвестору/гостю блок комиссии не отдаётся (A2) | тест catalog-гейта: инвестор → нет блока |
| AC7 | Расчёт в каталоге (gross, удержание ПИН, пул) совпадает с цифрами будущего начисления при неизменных правилах и условиях | сквозной тест UC-RWD-002 → UC-RWD-003 на одном объекте недвижимости и агентстве |
| AC8 | Net-цифры: 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).
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API метод:
../api/internal/calculate-property-object-commission.md
(контракт запроса/ответа,
AppliedPropertyObjectCommissionModel). - Domain entities: ../domain/reward-rule.md (алгоритм
ResolveRule, семантикаConditions, fail-closed для unknownPropertyType), ../domain/platform-commission-terms.md (резолюция удержания ПИН — net-цифры). - Формулы сумм: ../domain/commission-accrual.md (INV-2, INV-10).
- Потребитель:
../../catalog/README.md— витрина, кэш и consumerreward-rule-changed(../../catalog/consumers/— будущий путь по конвенции). - Связанные UC: UC-RWD-001 (откуда берутся правила), UC-RWD-007 (условия платформы), UC-RWD-003 (те же формулы при начислении).