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

UC-RWD-001. Застройщик настраивает правила вознаграждений агентов

Назначение

Описывает сценарий настройки системы вознаграждений застройщиком: менеджер застройщика создаёт правило вознаграждения — процент от базовой цены (до ремонта/скидки) или от конечной цены (базовая + ремонт) — с условиями-предикатами по типу объекта (квартира/парковка/коммерция), планировке (LayoutId), группе планировок (LayoutGroupId), количеству комнат и диапазону площади, задаёт приоритет разрешения конфликтов и активирует правило. Связан с контекстом rewards.

Metadata

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

Актор

ПолеЗначение
РольДевелопер (менеджер застройщика)
Permissions / scoperewards.rules.manage в scope Organization(CompanyId); чтение списка — rewards.rules.view
TenantTenantId = CompanyId застройщика (Type = Developer, ADR-0052); правило создаётся в этом tenant
Record-levelне применяется (правила — tenant-scope)

Предусловие

  • Компания-застройщик актора Status = Active; актор аутентифицирован (JWT identity).
  • Реплика справочника планировок Layout/LayoutGroup наполнена consumer-ами pin.chessboard.layout-upserted.v1 / pin.chessboard.layout-group-upserted.v1 (../consumers/chessboard-dimensions.md) — нужна для валидации LayoutIds/LayoutGroupIds при активации (INV-4 reward-rule.md).
  • Желаемый Priority не занят другим Active правилом застройщика (INV-3) — иначе поток A2.

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементЛК застройщика → «Вознаграждения агентов» → «Создать правило»; карточка правила → «Активировать»

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

  1. Менеджер открывает раздел «Вознаграждения агентов» (GET /rewards/developer/reward-rules, permission rewards.rules.view, список правил застройщика с пагинацией) и нажимает «Создать правило».
  2. Менеджер заполняет форму: название, база расчёта (BasePrice «от базовой цены» / FinalPrice «от конечной цены»), процент, приоритет, период действия и условия (тип объекта, планировки, группы планировок, диапазон комнат, диапазон площади; пустое измерение = «любое значение»). Списки планировок и групп фронт получает из chessboard (лукапы застройщика).
  3. Фронт вызывает POST /rewards/developer/reward-rules с телом CreateRewardRuleForm { name (1..256), description? (<= 2000), calculationBase (BasePrice | FinalPrice), commissionPercent (decimal > 0, <= 100, scale 2), priority (int 0..100000), effectiveFrom? (DateTimeOffset), effectiveTo? (DateTimeOffset, > effectiveFrom), conditions: { propertyTypes?: PropertyType[] (<= 3, уникальны), layoutIds?: Guid[] (<= 500, уникальны), layoutGroupIds?: Guid[] (<= 200, уникальны), roomsRange?: NullableRange<int> (границы min/max 0..20, min <= max), areaRangeM2?: NullableRange<decimal> (границы min/max > 0..10000, scale 2, min <= max) } }.
  4. Система создаёт правило-черновик:
    • Доступ. Менеджер с rewards.rules.manage в scope Organization(TenantId застройщика); иначе — 403 FORBIDDEN.
    • Проверка входа. 0 < commissionPercent <= 100 (INV-1, иначе REWARDS_RULE_PERCENT_OUT_OF_RANGE); согласованность пар roomsRange, areaRangeM2, effectiveFrom/effectiveTo (INV-2, иначе REWARDS_RULE_RANGE_INVALID); лимиты и уникальность элементов списков условий (иначе ValidationError). Существование планировок на создании черновика не проверяется — проверка при активации (шаг 7).
    • Создание. Правило создаётся в статусе Draft (Version = 1): поля — из формы, TenantId — застройщик актора, Conditions — из формы (пустое измерение = «любое значение»). Поднимается доменное событие RewardRuleCreatedEvent; Kafka-событие не публикуется — черновик не влияет на подбор комиссии.
    • Результат. { ruleId, status = Draft, version }.
  5. Фронт показывает карточку правила в статусе «Черновик»; менеджер проверяет условия и нажимает «Активировать».
  6. Фронт вызывает POST /rewards/developer/reward-rules/{ruleId}/activate (заголовок If-Match / поле rowVersion — optimistic concurrency).
  7. Система активирует правило:
    • Доступ. rewards.rules.manage. Правило существует в tenant застройщика; чужое/несуществующее — 404 NOT FOUND.
    • Guard состояния. Правило в Status = Draft (переход Draft → Active); иначе — REWARDS_RULE_INVALID_STATUS_TRANSITION.
    • INV-3 (приоритет свободен). Среди активных правил застройщика нет правила с тем же Priority; иначе — REWARDS_RULE_PRIORITY_TAKEN (409). Конкурентная гонка за приоритет ловится частично-уникальным индексом ux_reward_rules_tenant_priority_active (конфликт → REWARDS_RULE_PRIORITY_TAKEN, 409).
    • INV-4 (планировки существуют). Все conditions.layoutIds и conditions.layoutGroupIds есть в реплике справочника планировок и принадлежат застройщику TenantId; иначе — REWARDS_RULE_LAYOUT_NOT_FOUND (400 со списком отсутствующих id).
    • Переход и событие. Правило переходит Draft → Active (Version++, штамп актора); поднимается доменное событие RewardRuleActivatedEvent. В одном согласованном изменении публикуется RewardRuleChangedKafkaEvent (topic pin.rewards.reward-rule-changed.v1, key = Id; payload Id, TenantId, Status = Active, CalculationBase, CommissionPercent, Priority, EffectiveFrom, EffectiveTo, Conditions, Version).
    • Результат. { ruleId, status = Active, version }.
  8. Фронт обновляет список правил (invalidate query); catalog получает pin.rewards.reward-rule-changed.v1 и инвалидирует/пересчитывает денормализованную комиссию в витрине — агент увидит новую комиссию на объектах недвижимости (UC-RWD-002).

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

A1. Редактирование черновика

  1. PUT /rewards/developer/reward-rules/{ruleId} с UpdateRewardRuleForm (те же поля, что в шаге 3, плюс rowVersion) — guard INV-5 (Status = Draft, иначе REWARDS_RULE_ACTIVE_IMMUTABLE), полная замена полей и Conditions, Version++. Kafka-событие не публикуется. Возвращается к шагу 5.

A2. Приоритет занят активным правилом

  1. На шаге 7 среди активных правил застройщика найдено правило с тем же PriorityREWARDS_RULE_PRIORITY_TAKEN (409). Фронт показывает конфликтующее правило и предлагает другой приоритет. Менеджер меняет Priority черновика (A1) и повторяет шаг 6.

A3. Правка активного правила (деактивация)

  1. Поля активного правила иммутабельны (INV-5), кроме EffectiveTo. Для правки менеджер вызывает POST /rewards/developer/reward-rules/{ruleId}/deactivate — переход Active → Draft, публикуется RewardRuleChangedKafkaEvent (Status = Draft — catalog убирает правило из подбора). Далее A1 и повторная активация (шаги 6–8). Уже созданные начисления не пересчитываются — в них snapshot (решение №4 ../README.md).

A4. Досрочное завершение действия активного правила

  1. PATCH /rewards/developer/reward-rules/{ruleId}/effective-to { effectiveTo, rowVersion } — единственная разрешённая мутация в Active (INV-5); guard effectiveTo > now и > effectiveFrom. Публикуется RewardRuleChangedKafkaEvent. С момента EffectiveTo правило перестаёт участвовать в подборе (предикат выборки ResolveRule, шаг 1).

A5. Архивирование правила

  1. POST /rewards/developer/reward-rules/{ruleId}/archive — из Draft или Active; правило переходит в Status = Archived и проставляется DeletedAt (INV-6), поднимается доменное событие RewardRuleArchivedEvent; из Active дополнительно публикуется RewardRuleChangedKafkaEvent. Физическое удаление запрещено — на правило ссылаются начисления (AppliedRuleId, restrict).

A6. Планировка отсутствует в реплике

  1. На шаге 7 в реплике справочника найдены не все планировки (планировка не существует, принадлежит другому застройщику или синк справочника ещё не догнал) → REWARDS_RULE_LAYOUT_NOT_FOUND (400 со списком отсутствующих id). Менеджер проверяет планировки в chessboard и повторяет активацию после прихода синка.

Постусловие

  • Создан RewardRuleEntity (TenantId = застройщик, Status = Active) с валидными условиями и уникальным среди активных правил приоритетом.
  • RewardRuleChangedKafkaEvent опубликован в том же согласованном изменении. Обновление денормализованной комиссии в витрине catalogасинхронное (consumer catalog): окно eventual-consistency обычно секунды, в течение которого агент может видеть в каталоге прежнюю комиссию; расчёт при завершении сделки (UC-RWD-003) всегда идёт по актуальным правилам на момент CompletedAt, а не по кэшу каталога.
  • Правило участвует в подборе ResolveRule для расчёта комиссии в каталоге (UC-RWD-002) и в начислениях по сделкам (UC-RWD-003) с учётом периода EffectiveFrom/EffectiveTo.

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

КодHTTPУсловиеПоведение клиента
403 FORBIDDEN403Нет rewards.rules.manage в scope организации (INV-7)Скрыть кнопки управления правилами
ValidationError400Нарушение формата формы / лимитов списков условийПодсветить поля формы
REWARDS_RULE_PERCENT_OUT_OF_RANGE400commissionPercent <= 0 или > 100 (INV-1)Подсветить поле процента
REWARDS_RULE_RANGE_INVALID400roomsRange.min > roomsRange.max / areaRangeM2.min > areaRangeM2.max / effectiveTo <= effectiveFrom (INV-2)Подсветить диапазон
REWARDS_RULE_PRIORITY_TAKEN409Приоритет занят активным правилом (INV-3), включая гонку по unique indexПоказать конфликтующее правило, предложить другой приоритет
REWARDS_RULE_LAYOUT_NOT_FOUND400LayoutIds/LayoutGroupIds не найдены в реплике или чужого застройщика (INV-4)Показать отсутствующие id, обновить выбор планировок
REWARDS_RULE_ACTIVE_IMMUTABLE409Попытка Update не-черновика (INV-5)Предложить деактивацию (A3)
REWARDS_RULE_INVALID_STATUS_TRANSITION409Активация не из Draft / повторная активацияОбновить карточку правила
409 CONFLICT (RowVersion)409Конкурентное редактирование (оптимистическая блокировка)Перечитать правило и повторить
404 NOT FOUND404Правило не существует / чужой tenantВернуться к списку

Доменные События

СобытиеКогда поднимаетсяSubscribers
RewardRuleCreatedEventУспешный Create (шаг 4.4)in-proc аудит
RewardRuleActivatedEventDraft → Active (шаг 7.4)in-proc аудит
RewardRuleArchivedEvent* → Archived (A5)in-proc аудит

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

СобытиеTopicКогда публикуетсяConsumers
RewardRuleChangedKafkaEvent.v1pin.rewards.reward-rule-changed.v1Activate / Deactivate / Archive из Active / изменение EffectiveTocatalog — инвалидация/обновление денормализованной комиссии объектов недвижимости в витрине

Acceptance Criteria

#КритерийПроверка
AC1Правило создаётся с любой комбинацией 5 измерений условий; пустое измерение = «любое значение»unit-тесты RewardRuleConditionsDocument + ResolveRule
AC2Активация с занятым приоритетом невозможна, включая конкурентную гонку (INV-3)повторная активация с тем же Priority → 409; параллельные активации → одна успешна
AC3Активация с несуществующим LayoutId отклоняется (INV-4)тест с случайным Guid → 400 REWARDS_RULE_LAYOUT_NOT_FOUND
AC4Поля активного правила иммутабельны, кроме EffectiveTo (INV-5)PUT активного → 409; PATCH effective-to → 200
AC5Activate публикует RewardRuleChangedKafkaEvent атомарно с переходом в Activeконтракт-тест события + проверка атомарности публикации
AC6Архив сохраняет ссылки начислений (AppliedRuleId, restrict)архив правила с начислениями → начисления читаемы, snapshot цел
AC7Менеджер другого застройщика не видит и не меняет правило (tenant-изоляция)запрос из чужого tenant → 404

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

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

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

  • API методы: ../api/admin/create-reward-rule.md, ../api/admin/update-reward-rule.md, ../api/admin/activate-reward-rule.md, ../api/admin/archive-reward-rule.md, ../api/admin/list-reward-rules.md (пути по конвенции, api-method-template).
  • Domain entity: ../domain/reward-rule.md (поля, INV-1..7, stateDiagram, алгоритм ResolveRule).
  • Consumer реплики планировок: ../consumers/chessboard-dimensions.md; справочник — ../../chessboard/domain/layout.md, ../../chessboard/domain/layout-group.md (будущие пути по конвенции).
  • Связанные UC: UC-RWD-002 (комиссия в каталоге), UC-RWD-003 (применение правила при начислении).