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

PUT /api/v1/reward-rules/{ruleId} — Изменение правила вознаграждения

Полная правка правила в статусе Draft; для правила в статусе Active допускается изменение только EffectiveTo (досрочное завершение действия) — INV-5 domain/reward-rule.md.

Назначение

Менеджер застройщика редактирует черновик правила (все поля) или досрочно завершает действующее правило (EffectiveTo). Owning-контекст — rewards. Feature group — rules.

Metadata

ПолеЗначение
Сервис/контекстrewards
API areadeveloper-admin
Feature grouprules
Статусdraft
Документdocs/06-services/rewards/api/admin/update-reward-rule.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (менеджер компании-застройщика)
Auth policy / Permissionsrewards.rules.manage
Scope (RBAC)организация: компания Type = Developer
Record-levelне применяется
Tenant isolationTenantId из claims; чужое правило невидимо → 404
Auditupdate: UpdatedByUserId = ActorUserId, Version + 1

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/v1/reward-rules/{ruleId}
Success status200
Idempotency headerне применяется (естественная идемпотентность PUT + ExpectedVersion)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
ActorUserIdidentity claims (sub)GuidДаRequired
RuleIdroute {ruleId}GuidДаRequired, непустой
ExpectedVersionbodylongДа>= 1; сверка с Version агрегатаoptimistic concurrency
NamebodystringДа1..256
Descriptionbodystring?Нет<= 2000
CalculationBasebodyCommissionCalculationBaseДаenum BasePrice/FinalPrice
CommissionPercentbodydecimalДа> 0, <= 100, scale 2 (INV-1)
PrioritybodyintДа0..100000
EffectiveFrombodyDateTimeOffset?Нет
EffectiveTobodyDateTimeOffset?Нет> EffectiveFrom (INV-2); для Active — единственное изменяемое поле, дополнительно EffectiveTo >= UtcNow (нельзя завершить задним числом)
ConditionsbodyRewardRuleConditionsModelДапо таблице модели (README.md); ссылки ProjectIds/LayoutGroupIds/LayoutIds валидируются при активации (INV-4), непересечение периодов той же специфичности — тоже (INV-9)

PUT — полная замена: клиент присылает все поля (форма редактирования полностью заполнена значениями из get-reward-rule.md).

Модель ответа UpdateRewardRuleCommandResult

ПолеТипОбязательностьИсточникMaskingОписание
RuleRewardRuleModelДаобновлённый агрегатПолная проекция после правки (в т.ч. новый Version).

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер компании-застройщика; scope — организация застройщика (TenantId); требуется право rewards.rules.manage.
  2. Проверка входа и предусловий. Синтаксис и диапазоны полей — как при создании (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).
  3. Правка и переход.
    • Черновик (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); фиксируется новая версия.
  4. События. Для ветки Active (смена EffectiveTo) публикуется RewardRuleChangedKafkaEvent (топик pin.rewards.reward-rule-changed.v1, ключ = RuleId; payload по дому domain/reward-rule.md) — catalog пере-резолвит комиссию объектов недвижимости. Для ветки Draft интеграционного события нет (черновик не виден снаружи). Изменение правила и событие сохраняются в одном согласованном изменении.
  5. Результат. Полная проекция после правки (RewardRuleModel, включая новый Version) → 200.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsRewardRuleChangedKafkaEvent / pin.rewards.reward-rule-changed.v1 / key = RuleId — только при изменении EffectiveTo активного правила
External callsnone
Cache / projections / searchcatalog инвалидирует денормализованную комиссию по событию
Notificationsnone

Интеграционная последовательность (ветка Active, смена EffectiveTo): правило и факт публикации фиксируются в одном согласованном изменении; событие уходит асинхронно. Потребитель catalog пере-резолвит комиссию затронутых объектов недвижимости по подбору активных правил на момент asOf (domain/reward-rule.md, «Алгоритм подбора»). Окно eventual-consistency — секунды: в промежутке между коммитом и обработкой события агент в каталоге может видеть прежнюю комиссию (правило ещё числится действующим до наступления нового EffectiveTo); на уже созданные начисления смена EffectiveTo не влияет — в них зафиксирован снапшот правила.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
REWARDS_RULE_NOT_FOUND404Правило не существует / чужой tenant«Правило не найдено»no retry
REWARDS_RULE_ACTIVE_IMMUTABLE409Правка полей кроме EffectiveTo у Active; правка Archived (INV-5)«Действующее правило нельзя изменить — создайте новое»no retry
REWARDS_RULE_PERCENT_OUT_OF_RANGE400INV-1«Процент вне допустимого диапазона»no retry
REWARDS_RULE_RANGE_INVALID400INV-2«Некорректный диапазон условий»no retry
REWARDS_CONCURRENCY_CONFLICT409ExpectedVersion/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); правило ArchivedREWARDS_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).

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

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

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