UC-RWD-001. Застройщик настраивает правила вознаграждений агентов
Назначение
Описывает сценарий настройки системы вознаграждений застройщиком: менеджер застройщика
создаёт правило вознаграждения — процент от базовой цены (до ремонта/скидки) или от конечной цены
(базовая + ремонт) — с условиями-предикатами по типу объекта (квартира/парковка/коммерция), планировке
(LayoutId), группе планировок (LayoutGroupId), количеству комнат и диапазону площади, задаёт приоритет
разрешения конфликтов и активирует правило. Связан с контекстом rewards.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-RWD-001 |
| Контекст | rewards (см. docs/06-services/rewards/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Девелопер (менеджер застройщика) |
| Permissions / scope | rewards.rules.manage в scope Organization(CompanyId); чтение списка — rewards.rules.view |
| Tenant | TenantId = CompanyId застройщика (Type = Developer, ADR-0052); правило создаётся в этом tenant |
| Record-level | не применяется (правила — tenant-scope) |
Предусловие
- Компания-застройщик актора
Status = Active; актор аутентифицирован (JWTidentity). - Реплика справочника планировок
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 |
| Триггерящий элемент | ЛК застройщика → «Вознаграждения агентов» → «Создать правило»; карточка правила → «Активировать» |
Основной Поток
- Менеджер открывает раздел «Вознаграждения агентов» (
GET /rewards/developer/reward-rules, permissionrewards.rules.view, список правил застройщика с пагинацией) и нажимает «Создать правило». - Менеджер заполняет форму: название, база расчёта (
BasePrice«от базовой цены» /FinalPrice«от конечной цены»), процент, приоритет, период действия и условия (тип объекта, планировки, группы планировок, диапазон комнат, диапазон площади; пустое измерение = «любое значение»). Списки планировок и групп фронт получает изchessboard(лукапы застройщика). - Фронт вызывает
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) } }. - Система создаёт правило-черновик:
- Доступ. Менеджер с
rewards.rules.manageв scopeOrganization(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 }.
- Доступ. Менеджер с
- Фронт показывает карточку правила в статусе «Черновик»; менеджер проверяет условия и нажимает «Активировать».
- Фронт вызывает
POST /rewards/developer/reward-rules/{ruleId}/activate(заголовокIf-Match/ полеrowVersion— optimistic concurrency). - Система активирует правило:
- Доступ.
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(topicpin.rewards.reward-rule-changed.v1, key =Id; payloadId, TenantId, Status = Active, CalculationBase, CommissionPercent, Priority, EffectiveFrom, EffectiveTo, Conditions, Version). - Результат.
{ ruleId, status = Active, version }.
- Доступ.
- Фронт обновляет список правил (invalidate query);
catalogполучаетpin.rewards.reward-rule-changed.v1и инвалидирует/пересчитывает денормализованную комиссию в витрине — агент увидит новую комиссию на объектах недвижимости (UC-RWD-002).
Альтернативные Потоки
A1. Редактирование черновика
PUT /rewards/developer/reward-rules/{ruleId}сUpdateRewardRuleForm(те же поля, что в шаге 3, плюсrowVersion) — guard INV-5 (Status = Draft, иначеREWARDS_RULE_ACTIVE_IMMUTABLE), полная замена полей иConditions,Version++. Kafka-событие не публикуется. Возвращается к шагу 5.
A2. Приоритет занят активным правилом
- На шаге 7 среди активных правил застройщика найдено правило с тем же
Priority→REWARDS_RULE_PRIORITY_TAKEN(409). Фронт показывает конфликтующее правило и предлагает другой приоритет. Менеджер меняетPriorityчерновика (A1) и повторяет шаг 6.
A3. Правка активного правила (деактивация)
- Поля активного правила иммутабельны (INV-5), кроме
EffectiveTo. Для правки менеджер вызываетPOST /rewards/developer/reward-rules/{ruleId}/deactivate— переходActive → Draft, публикуетсяRewardRuleChangedKafkaEvent(Status = Draft— catalog убирает правило из подбора). Далее A1 и повторная активация (шаги 6–8). Уже созданные начисления не пересчитываются — в них snapshot (решение №4 ../README.md).
A4. Досрочное завершение действия активного правила
PATCH /rewards/developer/reward-rules/{ruleId}/effective-to { effectiveTo, rowVersion }— единственная разрешённая мутация вActive(INV-5); guardeffectiveTo > nowи> effectiveFrom. ПубликуетсяRewardRuleChangedKafkaEvent. С моментаEffectiveToправило перестаёт участвовать в подборе (предикат выборкиResolveRule, шаг 1).
A5. Архивирование правила
POST /rewards/developer/reward-rules/{ruleId}/archive— изDraftилиActive; правило переходит вStatus = Archivedи проставляетсяDeletedAt(INV-6), поднимается доменное событиеRewardRuleArchivedEvent; изActiveдополнительно публикуетсяRewardRuleChangedKafkaEvent. Физическое удаление запрещено — на правило ссылаются начисления (AppliedRuleId, restrict).
A6. Планировка отсутствует в реплике
- На шаге 7 в реплике справочника найдены не все планировки (планировка не существует, принадлежит
другому застройщику или синк справочника ещё не догнал) →
REWARDS_RULE_LAYOUT_NOT_FOUND(400 со списком отсутствующих id). Менеджер проверяет планировки вchessboardи повторяет активацию после прихода синка.
Постусловие
- Создан
RewardRuleEntity (TenantId = застройщик, Status = Active)с валидными условиями и уникальным среди активных правил приоритетом. RewardRuleChangedKafkaEventопубликован в том же согласованном изменении. Обновление денормализованной комиссии в витринеcatalog— асинхронное (consumercatalog): окно eventual-consistency обычно секунды, в течение которого агент может видеть в каталоге прежнюю комиссию; расчёт при завершении сделки (UC-RWD-003) всегда идёт по актуальным правилам на моментCompletedAt, а не по кэшу каталога.- Правило участвует в подборе
ResolveRuleдля расчёта комиссии в каталоге (UC-RWD-002) и в начислениях по сделкам (UC-RWD-003) с учётом периодаEffectiveFrom/EffectiveTo.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
403 FORBIDDEN | 403 | Нет rewards.rules.manage в scope организации (INV-7) | Скрыть кнопки управления правилами |
ValidationError | 400 | Нарушение формата формы / лимитов списков условий | Подсветить поля формы |
REWARDS_RULE_PERCENT_OUT_OF_RANGE | 400 | commissionPercent <= 0 или > 100 (INV-1) | Подсветить поле процента |
REWARDS_RULE_RANGE_INVALID | 400 | roomsRange.min > roomsRange.max / areaRangeM2.min > areaRangeM2.max / effectiveTo <= effectiveFrom (INV-2) | Подсветить диапазон |
REWARDS_RULE_PRIORITY_TAKEN | 409 | Приоритет занят активным правилом (INV-3), включая гонку по unique index | Показать конфликтующее правило, предложить другой приоритет |
REWARDS_RULE_LAYOUT_NOT_FOUND | 400 | LayoutIds/LayoutGroupIds не найдены в реплике или чужого застройщика (INV-4) | Показать отсутствующие id, обновить выбор планировок |
REWARDS_RULE_ACTIVE_IMMUTABLE | 409 | Попытка Update не-черновика (INV-5) | Предложить деактивацию (A3) |
REWARDS_RULE_INVALID_STATUS_TRANSITION | 409 | Активация не из Draft / повторная активация | Обновить карточку правила |
409 CONFLICT (RowVersion) | 409 | Конкурентное редактирование (оптимистическая блокировка) | Перечитать правило и повторить |
404 NOT FOUND | 404 | Правило не существует / чужой tenant | Вернуться к списку |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
RewardRuleCreatedEvent | Успешный Create (шаг 4.4) | in-proc аудит |
RewardRuleActivatedEvent | Draft → Active (шаг 7.4) | in-proc аудит |
RewardRuleArchivedEvent | * → Archived (A5) | in-proc аудит |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
RewardRuleChangedKafkaEvent.v1 | pin.rewards.reward-rule-changed.v1 | Activate / Deactivate / Archive из Active / изменение EffectiveTo | catalog — инвалидация/обновление денормализованной комиссии объектов недвижимости в витрине |
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 |
| AC5 | Activate публикует RewardRuleChangedKafkaEvent атомарно с переходом в Active | контракт-тест события + проверка атомарности публикации |
| AC6 | Архив сохраняет ссылки начислений (AppliedRuleId, restrict) | архив правила с начислениями → начисления читаемы, snapshot цел |
| AC7 | Менеджер другого застройщика не видит и не меняет правило (tenant-изоляция) | запрос из чужого tenant → 404 |
Обратные ссылки
Автоссылки: где используется этот документ.
- scenarios (1): UC-OVR-010. От завершённой сделки до выплаты комиссии и реферального вознаграждения
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- 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 (применение правила при начислении).