POST /api/v1/reward-rules/{ruleId}/archive — Архивирование правила вознаграждения
«Удаление» правила в терминах API: перевод Draft|Active → Archived + soft-delete (DeletedAt).
Физическое удаление запрещено — на правило ссылаются начисления (AppliedRuleId, restrict) —
domain/reward-rule.md, INV-6.
Назначение
Менеджер застройщика навсегда выводит правило из работы (исторические начисления сохраняются).
Owning-контекст — rewards. Feature group — rules.
| Поле | Значение |
|---|
| Сервис/контекст | rewards |
| API area | developer-admin |
| Feature group | rules |
| Статус | draft |
| Документ | docs/06-services/rewards/api/admin/archive-reward-rule.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (менеджер компании-застройщика) |
| Auth policy / Permissions | rewards.rules.manage |
| Scope (RBAC) | организация: компания Type = Developer |
| Record-level | не применяется |
| Tenant isolation | TenantId из claims; чужое правило → 404 |
| Audit | delete (soft): DeletedAt, DeletedByUserId = ActorUserId, Version + 1, доменное событие RewardRuleArchivedEvent |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/reward-rules/{ruleId}/archive |
| Success status | 200 |
| Idempotency header | не применяется (повтор из Archived → 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 |
Модель ответа ArchiveRewardRuleCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | RewardRuleEntity.Id | — | Id правила. |
Status | string (enum RewardRuleStatus) | Да | Status | — | Всегда Archived. |
Version | long | Да | Version | — | Новая версия. |
Предусловия и постусловия
- Предусловия. Актор — менеджер застройщика с
rewards.rules.manage; правило существует в его
tenant-е и находится в статусе Draft или Active; клиент передаёт актуальный ExpectedVersion.
- Постусловия (успех). Правило в статусе
Archived и одновременно помечено удалённым (DeletedAt
заполнён — INV-6); Version увеличен; правило навсегда выведено из подбора; исторические начисления
сохраняют ссылку AppliedRuleId (физического удаления нет). Если правило было Active —
опубликовано RewardRuleChangedKafkaEvent со Status = Archived; если было Draft — события нет.
- Постусловия (отказ). Правило без изменений; событие не публикуется.
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер компании-застройщика; scope — организация
застройщика (
TenantId); требуется право rewards.rules.manage.
- Проверка входа и предусловий.
RuleId непустой, ExpectedVersion >= 1. Правило существует
и принадлежит tenant-у застройщика — иначе REWARDS_RULE_NOT_FOUND. Статус ∈ {Draft, Active};
из Archived — REWARDS_RULE_INVALID_STATUS_TRANSITION. Version совпадает с ExpectedVersion —
иначе REWARDS_CONCURRENCY_CONFLICT. Наличие начислений архив не блокирует: ссылки на правило
остаются живыми (физического удаления нет).
- Переход и запись. Правило переходит в
Archived и одновременно помечается удалённым
(DeletedAt) — согласованность архива (INV-6); фиксируются актор и новая версия. Soft-delete:
правило остаётся в хранилище (на него ссылаются исторические начисления).
- События. Если правило было
Active — публикуется RewardRuleChangedKafkaEvent
(топик pin.rewards.reward-rule-changed.v1, ключ = RuleId; payload по дому, Status = Archived)
— catalog пересчитывает комиссию. Для архивации из Draft интеграционного события нет
(правило не было видно снаружи).
- Результат.
Id, Status = Archived, новый Version → 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | RewardRuleChangedKafkaEvent / pin.rewards.reward-rule-changed.v1 / key = RuleId — только при архивации из Active |
| External calls | none |
| Cache / projections / search | catalog пересчитывает комиссию по событию |
| Notifications | none |
Окно eventual-consistency: при архивации из Active событие RewardRuleChangedKafkaEvent (Status = Archived) уходит через outbox атомарно с переходом (ключ = RuleId, порядок событий по правилу
сохраняется). catalog асинхронно убирает/пересчитывает денормализованную комиссию объектов
недвижимости, попадавших под правило — окно от коммита до обновления витрины измеряется секундами. В этом
промежутке витрина может кратко показывать прежнюю комиссию, тогда как внутренний расчёт rewards
(../internal/calculate-property-object-commission.md)
правило уже не применяет; после снятия объект недвижимости может перейти под другое активное правило
меньшего приоритета либо остаться без комиссии. При архивации из Draft интеграционного события нет и
витрина не затрагивается (черновик не был виден снаружи). Уже созданные начисления не пересчитываются —
они хранят snapshot правила (решение №4).
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
REWARDS_RULE_NOT_FOUND | 404 | Правило не существует / чужой tenant | «Правило не найдено» | no retry |
REWARDS_RULE_INVALID_STATUS_TRANSITION | 409 | Уже Archived | «Правило уже в архиве» | no retry |
REWARDS_CONCURRENCY_CONFLICT | 409 | ExpectedVersion/xmin конфликт | «Правило изменено другим пользователем» | retry после refresh |
Acceptance-критерии
- Архив действующего. Дано: правило в
Active, ExpectedVersion актуален. Когда: POST .../archive. Тогда: 200, Status = Archived, DeletedAt заполнён (INV-6), Version + 1;
опубликовано RewardRuleChangedKafkaEvent со Status = Archived; catalog пересчитает комиссию.
- Архив черновика. Дано: правило в
Draft. Когда: POST .../archive. Тогда: 200, Status = Archived; интеграционного события нет (черновик не был виден снаружи).
- Повторный архив. Дано: правило уже
Archived. Когда: POST .../archive. Тогда: 409
REWARDS_RULE_INVALID_STATUS_TRANSITION.
- Начисления сохраняются. Дано: на правило ссылаются начисления (
AppliedRuleId). Когда: POST .../archive. Тогда: 200; начисления и их ссылка на правило остаются живыми (restrict, INV-6).
- Чужой tenant. Дано: правило принадлежит другому застройщику. Когда:
POST .../archive. Тогда:
404 REWARDS_RULE_NOT_FOUND (существование чужого правила не раскрывается).
- Конкурентная версия. Дано:
ExpectedVersion устарел. Когда: POST. Тогда: 409
REWARDS_CONCURRENCY_CONFLICT.
Совместимость и наблюдаемость
- Архив терминален — восстановление не проектируется (создаётся новое правило копированием формы).
- Logs:
TenantId, RuleId, исходный статус; metrics: rewards_rule_archive_total{result};
traces: OTel; dashboard rewards-overview; runbook — не применяется.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы