POST /api/v1/reward-rules/{ruleId}/deactivate — Возврат правила в черновик
Перевод правила Active → Draft для правки: правило немедленно выводится из подбора комиссии
(RewardRuleResolver его больше не видит), уже созданные начисления не затрагиваются (в них snapshot).
Назначение
Менеджер застройщика снимает действующее правило для редактирования (Active-правила иммутабельны,
INV-5 domain/reward-rule.md). Owning-контекст — rewards. Feature group —
rules.
| Поле | Значение |
|---|
| Сервис/контекст | rewards |
| API area | developer-admin |
| Feature group | rules |
| Статус | draft |
| Документ | docs/06-services/rewards/api/admin/deactivate-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, Version + 1 |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/reward-rules/{ruleId}/deactivate |
| Success status | 200 |
| Idempotency header | не применяется (повтор из Draft → REWARDS_RULE_INVALID_STATUS_TRANSITION) |
| 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 | optimistic concurrency |
Модель ответа DeactivateRewardRuleCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Rule | RewardRuleModel | Да | обновлённый агрегат | — | Проекция после перехода (Status = Draft). |
Предусловия и постусловия
- Предусловия. Актор — менеджер застройщика с
rewards.rules.manage; правило существует в его
tenant-е и находится в статусе Active; клиент передаёт актуальный ExpectedVersion.
- Постусловия (успех). Правило в статусе
Draft, Version увеличен; приоритет освобождён (доступен
другому правилу для активации); период правила освобождает слот его сигнатуры специфичности (другое
правило той же специфичности может занять пересекающийся период, INV-9); правило выведено из подбора
комиссии; опубликовано RewardRuleChangedKafkaEvent со Status = Draft. Уже созданные начисления не
затронуты — они хранят snapshot правила (решение №4).
- Постусловия (отказ). Правило остаётся
Active/Draft/Archived без изменений, событие не
публикуется.
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер компании-застройщика; scope — организация
застройщика (
TenantId); требуется право rewards.rules.manage.
- Проверка входа и предусловий.
RuleId непустой, ExpectedVersion >= 1. Правило существует
и принадлежит tenant-у застройщика — иначе REWARDS_RULE_NOT_FOUND. Статус = Active — иначе
REWARDS_RULE_INVALID_STATUS_TRANSITION. Version совпадает с ExpectedVersion — иначе
REWARDS_CONCURRENCY_CONFLICT. Уже созданные начисления не проверяются: они хранят snapshot и не
ломаются (решение №4 ../README.md).
- Переход и запись. Правило переходит
Active → Draft; фиксируются актор и новая версия.
Приоритет освобождается (среди активных правило больше не учитывается).
- События. Публикуется
RewardRuleChangedKafkaEvent
(топик pin.rewards.reward-rule-changed.v1, ключ = RuleId; payload по дому, Status = Draft) —
catalog убирает/пересчитывает комиссию объектов недвижимости, попадавших под правило.
- Результат. Проекция обновлённого правила (
RewardRuleModel, Status = Draft) → 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | RewardRuleChangedKafkaEvent / pin.rewards.reward-rule-changed.v1 / key = RuleId |
| External calls | none |
| Cache / projections / search | catalog пересчитывает комиссию по событию |
| Notifications | none |
Окно eventual-consistency: событие уходит через outbox атомарно с переходом Active → Draft (ключ =
RuleId). catalog асинхронно убирает/пересчитывает комиссию объектов недвижимости, попадавших под
правило — окно секунды. В промежутке витрина может кратко показывать прежнюю комиссию, тогда как
внутренний расчёт rewards уже её не применяет; после снятия правила объект недвижимости может
перейти под другое активное правило меньшего приоритета либо остаться без комиссии.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
REWARDS_RULE_NOT_FOUND | 404 | Правило не существует / чужой tenant | «Правило не найдено» | no retry |
REWARDS_RULE_INVALID_STATUS_TRANSITION | 409 | Status != Active | «Деактивировать можно только действующее правило» | no retry |
REWARDS_CONCURRENCY_CONFLICT | 409 | ExpectedVersion/xmin конфликт | «Правило изменено другим пользователем» | retry после refresh |
Acceptance-критерии
- Happy path. Дано: правило в
Active, ExpectedVersion актуален. Когда: POST .../deactivate.
Тогда: 200, Status = Draft, Version + 1, приоритет освобождён; опубликовано
RewardRuleChangedKafkaEvent; catalog пересчитает комиссию в окне секунд.
- Не действующее правило. Дано: правило в
Draft или Archived. Когда: POST .../deactivate.
Тогда: 409 REWARDS_RULE_INVALID_STATUS_TRANSITION.
- Начисления не затронуты. Дано: по правилу уже есть начисления. Когда:
POST .../deactivate.
Тогда: 200; существующие начисления сохраняют свои суммы и snapshot (не пересчитываются).
- Освобождение приоритета. Дано: правило A
Active с Priority = P. Когда: A деактивировано, затем
правило B с Priority = P активируется. Тогда: активация B успешна (приоритет P свободен).
- Конкурентная версия. Дано:
ExpectedVersion устарел. Когда: POST. Тогда: 409
REWARDS_CONCURRENCY_CONFLICT.
Совместимость и наблюдаемость
- Изменений контракта нет — additive-only, как у остальных rule-операций.
- Logs:
TenantId, RuleId; metrics: rewards_rule_deactivate_total{result}; traces: OTel;
dashboard rewards-overview; runbook — не применяется.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы