PUT /api/v1/reward-rules/{ruleId} — Изменение правила вознаграждения
Полная правка правила в статусе Draft; для правила в статусе Active допускается изменение
только EffectiveTo (досрочное завершение действия) — INV-5
domain/reward-rule.md.
Назначение
Менеджер застройщика редактирует черновик правила (все поля) или досрочно завершает действующее
правило (EffectiveTo). Owning-контекст — rewards. Feature group — rules.
| Поле | Значение |
|---|
| Сервис/контекст | rewards |
| API area | developer-admin |
| Feature group | rules |
| Статус | draft |
| Документ | docs/06-services/rewards/api/admin/update-reward-rule.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (менеджер компании-застройщика) |
| Auth policy / Permissions | rewards.rules.manage |
| Scope (RBAC) | организация: компания Type = Developer |
| Record-level | не применяется |
| Tenant isolation | TenantId из claims; чужое правило невидимо → 404 |
| Audit | update: UpdatedByUserId = ActorUserId, Version + 1 |
HTTP-контракт
| Поле | Значение |
|---|
| Method | PUT |
| Route | /api/v1/reward-rules/{ruleId} |
| Success status | 200 |
| Idempotency header | не применяется (естественная идемпотентность PUT + ExpectedVersion) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
ActorUserId | identity claims (sub) | Guid | Да | Required | — |
RuleId | route {ruleId} | Guid | Да | Required, непустой | — |
ExpectedVersion | body | long | Да | >= 1; сверка с Version агрегата | optimistic concurrency |
Name | body | string | Да | 1..256 | — |
Description | body | string? | Нет | <= 2000 | — |
CalculationBase | body | CommissionCalculationBase | Да | enum BasePrice/FinalPrice | — |
CommissionPercent | body | decimal | Да | > 0, <= 100, scale 2 (INV-1) | — |
Priority | body | int | Да | 0..100000 | — |
EffectiveFrom | body | DateTimeOffset? | Нет | — | — |
EffectiveTo | body | DateTimeOffset? | Нет | > EffectiveFrom (INV-2); для Active — единственное изменяемое поле, дополнительно EffectiveTo >= UtcNow (нельзя завершить задним числом) | — |
Conditions | body | RewardRuleConditionsModel | Да | по таблице модели (README.md); ссылки ProjectIds/LayoutGroupIds/LayoutIds валидируются при активации (INV-4), непересечение периодов той же специфичности — тоже (INV-9) | — |
PUT — полная замена: клиент присылает все поля (форма редактирования полностью заполнена значениями
из get-reward-rule.md).
Модель ответа UpdateRewardRuleCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Rule | RewardRuleModel | Да | обновлённый агрегат | — | Полная проекция после правки (в т.ч. новый Version). |
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер компании-застройщика; scope — организация
застройщика (
TenantId); требуется право rewards.rules.manage.
- Проверка входа и предусловий. Синтаксис и диапазоны полей — как при создании (INV-1/INV-2),
иначе
REWARDS_RULE_PERCENT_OUT_OF_RANGE / REWARDS_RULE_RANGE_INVALID / ValidationError.
Правило существует и принадлежит tenant-у застройщика — иначе REWARDS_RULE_NOT_FOUND. Статус ∈
{Draft, Active}; Archived не редактируется → REWARDS_RULE_ACTIVE_IMMUTABLE. Version
совпадает с ExpectedVersion — иначе REWARDS_CONCURRENCY_CONFLICT. Реплика планировок не
проверяется — валидация ссылок отложена до активации (INV-4).
- Правка и переход.
- Черновик (
Status = Draft) — заменяются все поля из тела (полная замена PUT), с проверкой
INV-1/INV-2; фиксируются актор и новая версия. Уникальность Priority среди активных правил (INV-3)
здесь не проверяется — черновик в подборе не участвует; коллизия приоритета всплывёт только при
активации (activate-reward-rule.md, REWARDS_RULE_PRIORITY_TAKEN), поэтому
редактировать черновик можно свободно;
- действующее правило (
Status = Active) — разрешено изменить только EffectiveTo (досрочное
завершение действия). Если любое другое присланное поле отличается от текущего значения —
REWARDS_RULE_ACTIVE_IMMUTABLE (INV-5); фиксируется новая версия.
- События. Для ветки
Active (смена EffectiveTo) публикуется RewardRuleChangedKafkaEvent
(топик pin.rewards.reward-rule-changed.v1, ключ = RuleId; payload по дому
domain/reward-rule.md) — catalog пере-резолвит комиссию объектов
недвижимости. Для ветки Draft интеграционного события нет (черновик не виден снаружи). Изменение
правила и событие сохраняются в одном согласованном изменении.
- Результат. Полная проекция после правки (
RewardRuleModel, включая новый Version) → 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | RewardRuleChangedKafkaEvent / pin.rewards.reward-rule-changed.v1 / key = RuleId — только при изменении EffectiveTo активного правила |
| External calls | none |
| Cache / projections / search | catalog инвалидирует денормализованную комиссию по событию |
| Notifications | none |
Интеграционная последовательность (ветка Active, смена EffectiveTo): правило и факт публикации фиксируются
в одном согласованном изменении; событие уходит асинхронно. Потребитель catalog пере-резолвит комиссию
затронутых объектов недвижимости по подбору активных правил на момент asOf
(domain/reward-rule.md, «Алгоритм подбора»). Окно eventual-consistency —
секунды: в промежутке между коммитом и обработкой события агент в каталоге может видеть прежнюю комиссию
(правило ещё числится действующим до наступления нового EffectiveTo); на уже созданные начисления смена
EffectiveTo не влияет — в них зафиксирован снапшот правила.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
REWARDS_RULE_NOT_FOUND | 404 | Правило не существует / чужой tenant | «Правило не найдено» | no retry |
REWARDS_RULE_ACTIVE_IMMUTABLE | 409 | Правка полей кроме EffectiveTo у Active; правка Archived (INV-5) | «Действующее правило нельзя изменить — создайте новое» | no retry |
REWARDS_RULE_PERCENT_OUT_OF_RANGE | 400 | INV-1 | «Процент вне допустимого диапазона» | no retry |
REWARDS_RULE_RANGE_INVALID | 400 | INV-2 | «Некорректный диапазон условий» | no retry |
REWARDS_CONCURRENCY_CONFLICT | 409 | ExpectedVersion/RowVersion конфликт | «Правило изменено другим пользователем» | retry после refresh |
Общие коды — README.md.
Acceptance criteria
- AC-1 (правка черновика). Given правило в статусе
Draft (Version = N), принадлежащее tenant-у
застройщика; When менеджер отправляет полную форму со всеми валидными полями и ExpectedVersion = N;
Then все поля заменяются значениями из тела, статус остаётся Draft, Version = N+1, актор
проставлен, интеграционное событие не публикуется (черновик снаружи не виден); ответ 200 с полной
проекцией RewardRuleModel.
- AC-2 (досрочное завершение действующего). Given правило
Active; When в теле изменён только
EffectiveTo на значение >= UtcNow, остальные поля равны текущим; Then обновляется только
EffectiveTo, Version + 1, публикуется RewardRuleChangedKafkaEvent
(pin.rewards.reward-rule-changed.v1, ключ RuleId), catalog пере-резолвит комиссию (окно
eventual-consistency — секунды); ответ 200.
- AC-3 (иммутабельность действующего). Given правило
Active; When в теле изменено любое поле
кроме EffectiveTo; Then отказ REWARDS_RULE_ACTIVE_IMMUTABLE (409), правило не меняется, событие не
публикуется.
- AC-4 (нельзя завершить задним числом). Given правило
Active; When EffectiveTo задан в
прошлом (< UtcNow); Then ошибка валидации EffectiveTo (400), изменение отклонено.
- AC-5 (границы значений). Given любой статус, допускающий правку; When
CommissionPercent вне
(0; 100] или диапазоны условий инвертированы (Min > Max) / EffectiveTo <= EffectiveFrom; Then
REWARDS_RULE_PERCENT_OUT_OF_RANGE (INV-1) либо REWARDS_RULE_RANGE_INVALID (INV-2), 400.
- AC-6 (конкурентная правка). Given правило параллельно деактивировано/изменено (версия выросла);
When приходит PUT с устаревшим
ExpectedVersion; Then REWARDS_CONCURRENCY_CONFLICT (409); клиент
перечитывает форму (get-reward-rule.md) и повторяет.
- AC-7 (чужое/архив). Given правило другого tenant или несуществующее —
REWARDS_RULE_NOT_FOUND
(404); правило Archived — REWARDS_RULE_ACTIVE_IMMUTABLE (409, INV-5).
Совместимость и наблюдаемость
- Контракт PUT полный: добавление нового optional поля требует одновременного обновления формы
(иначе PUT затрёт значение) — фиксируется в changelog клиента.
- Logs:
TenantId, ActorUserId, RuleId, ветка (draft-edit/close-period); metrics:
rewards_rule_update_total{result,branch}; traces: OTel; dashboard rewards-overview.
- НФТ (ADR-0056): правил у застройщика десятки, правка — точечная мутация одного агрегата (для ветки
Active + одно событие); целевая латентность p95 < 400 мс; частота — низкая (настройка правил в админке
застройщика, back-office).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы