POST /api/v1/reward-rules/{ruleId}/activate — Активация правила вознаграждения
Перевод правила Draft → Active: с этого момента правило участвует в подборе комиссии для каталога
и начислений (RewardRuleResolver). При активации проверяются ссылочная целостность условий каталога
ProjectIds/LayoutGroupIds/LayoutIds (INV-4), уникальность приоритета среди активных правил
(INV-3) и непересечение периодов EffectiveFrom/EffectiveTo среди активных правил той же сигнатуры
специфичности (INV-9 — детерминизм промо-акций) — domain/reward-rule.md.
Назначение
Менеджер застройщика вводит правило в действие. Owning-контекст — rewards. Feature group — rules.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | rewards |
| API area | developer-admin |
| Feature group | rules |
| Статус | draft |
| Документ | docs/06-services/rewards/api/admin/activate-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, доменное событие RewardRuleActivatedEvent |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/reward-rules/{ruleId}/activate |
| Success status | 200 |
| Idempotency header | не применяется (повтор из Active → 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; сверка с Version | optimistic concurrency |
Модель ответа ActivateRewardRuleCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Rule | RewardRuleModel | Да | обновлённый агрегат | — | Проекция после активации (Status = Active). |
Предусловия и постусловия
- Предусловия. Актор — менеджер застройщика с
rewards.rules.manage; правило существует в его tenant-е, в статусеDraft; клиент передаёт актуальныйExpectedVersion. Реплика справочникаchessboardсинхронизирована настолько, что всеProjectIds/LayoutGroupIds/LayoutIdsправила в ней присутствуют. Среди активных правил той же сигнатуры специфичности нет записи с пересекающимся периодом (INV-9). - Постусловия (успех). Правило в статусе
Active,Versionувеличен, зафиксирован актор; приоритет правила занят среди активных правил tenant-а (INV-3); период правила не пересекается с другими активными правилами той же специфичности (INV-9); правило участвует в подборе комиссии с учётом периодаEffectiveFrom/EffectiveTo; опубликованоRewardRuleChangedKafkaEventсоStatus = Active. - Постусловия (отказ). Правило остаётся в исходном статусе (
Draftили ужеActive/Archived), версия не меняется, событие не публикуется. - Граница «активно, но ещё не действует». Если задан
EffectiveFromв будущем — правило переходит вActive, но в подбор попадёт только с наступлениемEffectiveFrom; до этого моментаcatalogкомиссию по правилу не показывает (правилоActive, но вне периода).
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер компании-застройщика; scope — организация
застройщика (
TenantId); требуется правоrewards.rules.manage. - Проверка входа и предусловий.
RuleIdнепустой,ExpectedVersion >= 1. Правило существует и принадлежит tenant-у застройщика — иначеREWARDS_RULE_NOT_FOUND. Статус правила =Draft(единственный допустимый исходный) — иначеREWARDS_RULE_INVALID_STATUS_TRANSITION.Versionсовпадает сExpectedVersion— иначеREWARDS_CONCURRENCY_CONFLICT. ЕслиEffectiveToзадан и уже в прошлом —REWARDS_RULE_RANGE_INVALID(нельзя активировать истёкшее правило). - Проверка инвариантов активации.
- Уникальность приоритета (INV-3): среди активных правил застройщика нет другого с тем же
Priority— иначеREWARDS_RULE_PRIORITY_TAKEN. - Ссылочная целостность условий (INV-4): все
Conditions.ProjectIds,Conditions.LayoutGroupIdsиConditions.LayoutIdsприсутствуют в реплике справочникаchessboardзастройщика (не архивированы, consumer ../consumers/chessboard-dimensions.md) — недостающие ids возвращаются вREWARDS_RULE_LAYOUT_NOT_FOUND(перечислением в metadata ошибки). - Непересечение периодов внутри специфичности (INV-9): среди активных правил той же сигнатуры
специфичности (тот же набор заполненных измерений
Conditionsс теми же значениями) нет записи, чей период[EffectiveFrom, EffectiveTo|∞)пересекается с периодом активируемого правила — иначеREWARDS_REWARD_RULE_OVERLAP. Правила той же сигнатуры читаются под блокировкой ключа сигнатуры (детерминизм промо-акций: на любойasOfактивна не более одной записи специфичности).
- Уникальность приоритета (INV-3): среди активных правил застройщика нет другого с тем же
- Переход и запись. Правило переходит
Draft → Active;EffectiveFromостаётсяnull(действует сразу), если не задан; фиксируются актор и новая версия. Переход правила сохраняется в одном согласованном изменении. - События. Публикуется
RewardRuleChangedKafkaEvent(топикpin.rewards.reward-rule-changed.v1, ключ =RuleId; payload:Id,TenantId,Status = Active,CalculationBase,CommissionPercent,Priority,EffectiveFrom/To,Conditions,Version) — потребительcatalogобновляет комиссию в витрине. Гонка одинакового приоритета двух менеджеров ловится уникальным индексом активных правил и возвращаетREWARDS_RULE_PRIORITY_TAKEN. - Результат. Проекция обновлённого правила (
RewardRuleModel,Status = Active) →200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | RewardRuleChangedKafkaEvent / pin.rewards.reward-rule-changed.v1 / key = RuleId |
| External calls | none (реплика планировок локальная) |
| Cache / projections / search | catalog обновляет комиссию объектов недвижимости по событию |
| Notifications | none |
Окно eventual-consistency: RewardRuleChangedKafkaEvent публикуется через outbox атомарно с переходом
Draft → Active (ключ = RuleId, порядок событий по правилу сохраняется). catalog потребляет
асинхронно и пересчитывает денормализованную комиссию затронутых объектов недвижимости — окно от коммита
до обновления витрины измеряется секундами. В этом окне менеджер уже видит правило Active в админке,
а витрина показывает прежнюю комиссию; расчёт через внутренний API rewards
(../internal/calculate-property-object-commission.md)
отражает новое правило сразу после коммита.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
REWARDS_RULE_NOT_FOUND | 404 | Правило не существует / чужой tenant | «Правило не найдено» | no retry |
REWARDS_RULE_INVALID_STATUS_TRANSITION | 409 | Status != Draft | «Активировать можно только черновик» | no retry |
REWARDS_RULE_PRIORITY_TAKEN | 409 | Приоритет занят активным правилом (INV-3) | «Приоритет уже используется действующим правилом» | no retry |
REWARDS_REWARD_RULE_OVERLAP | 409 | Период пересекается с активным правилом той же сигнатуры специфичности (INV-9) | «Период пересекается с действующим правилом той же области» | no retry (сузить период или деактивировать конфликтующее) |
REWARDS_RULE_LAYOUT_NOT_FOUND | 400 | ProjectIds/LayoutGroupIds/LayoutIds нет в реплике chessboard застройщика (INV-4) | «Ссылка каталога не найдена» + ids в metadata | retry после синка chessboard |
REWARDS_RULE_RANGE_INVALID | 400 | EffectiveTo в прошлом | «Период действия уже истёк» | no retry |
REWARDS_CONCURRENCY_CONFLICT | 409 | ExpectedVersion/xmin конфликт | «Правило изменено другим пользователем» | retry после refresh |
Acceptance-критерии
- Happy path. Дано: правило в
Draft, приоритет свободен среди активных, всеLayoutIdsесть в реплике,ExpectedVersionактуален. Когда:POST .../activate. Тогда:200,Status = Active,Version + 1; опубликованоRewardRuleChangedKafkaEvent;catalogобновит комиссию в окне секунд. - Не черновик. Дано: правило уже
ActiveилиArchived. Когда:POST .../activate. Тогда:409REWARDS_RULE_INVALID_STATUS_TRANSITION, событие не публикуется. - Приоритет занят. Дано: среди активных правил tenant-а уже есть правило с тем же
Priority. Когда:POST .../activate. Тогда:409REWARDS_RULE_PRIORITY_TAKEN(INV-3); переход не выполнен. - Пересечение периодов промо. Дано: уже активно правило с сигнатурой
ProjectIds = [ЖК-X]и периодом[T1, T2); активируется второе правило той же сигнатуры (ProjectIds = [ЖК-X]) с периодом, пересекающим[T1, T2). Когда:POST .../activate. Тогда:409REWARDS_REWARD_RULE_OVERLAP(INV-9); переход не выполнен (сузить период или деактивировать конфликтующее правило). Правило иной сигнатуры (например безProjectIds— ставка на застройщика) активируется свободно: специфичности разные, пересечение не проверяется. - Ссылка каталога не в реплике. Дано:
Conditions.ProjectIds/LayoutGroupIds/LayoutIdsсодержит id, отсутствующий в реплике справочникаchessboardзастройщика. Когда:POST .../activate. Тогда:400REWARDS_RULE_LAYOUT_NOT_FOUNDсо списком недостающих ids; повтор допустим после синкаchessboard. - Истёкший период. Дано:
EffectiveToуже в прошлом. Когда:POST .../activate. Тогда:400REWARDS_RULE_RANGE_INVALID. - Активно, но ещё не действует. Дано: правило в
DraftсEffectiveFromв будущем, приоритет свободен,LayoutIdsв реплике. Когда:POST .../activate. Тогда:200,Status = Active; приоритет занят среди активных (INV-3), но в подбор комиссии правило попадёт только с наступлениемEffectiveFrom— до этого моментаcatalogи внутренний расчётrewardsкомиссию по нему не показывают/не применяют (правилоActive, но вне периода). - Гонка двух активаций. Дано: два менеджера активируют разные черновики с одинаковым
Priority. Когда: одновременныйPOST .../activate. Тогда: один —200, другой —409REWARDS_RULE_PRIORITY_TAKEN(страховка уникальным индексом активных правил). - Конкурентная версия. Дано:
ExpectedVersionне совпадает с текущимVersion. Когда:POST. Тогда:409REWARDS_CONCURRENCY_CONFLICT; клиент перечитывает правило и повторяет.
Совместимость и наблюдаемость
- Payload
RewardRuleChangedKafkaEvent— additive-only; breaking →pin.rewards.reward-rule-changed.v2. - Logs:
TenantId,RuleId,Priority; metrics:rewards_rule_activate_total{result}; traces: OTel; dashboardrewards-overview; runbook — не применяется.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (5): GET /api/v1/reward-rules/{ruleId} — Карточка правила вознаграждения, POST /api/v1/reward-rules — Создание правила вознаграждения (черновик), POST /api/v1/reward-rules/{ruleId}/deactivate — Возврат правила в черновик, PUT /api/v1/reward-rules/{ruleId} — Изменение правила вознаграждения, Rewards API — реестр операций и общие контрактные модели
- События (2): Event Contract: pin.rewards.commission-rules (CommissionRulesChangedKafkaEvent), Event Contract: pin.rewards.reward-rule-changed (RewardRuleChangedKafkaEvent)
- Консюмеры (1): Consumer: Chessboard Dimensions Sync (pin-rewards-chessboard-dimensions)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: domain/reward-rule.md (INV-3/4/9, stateDiagram).
- Реплика справочника
chessboard: ../consumers/chessboard-dimensions.md. - Обратный переход: deactivate-reward-rule.md.