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

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 areadeveloper-admin
Feature grouprules
Статусdraft
Документdocs/06-services/rewards/api/admin/activate-reward-rule.md

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

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

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
ActorUserIdidentity claims (sub)GuidДаRequired
RuleIdroute {ruleId}GuidДаRequired, непустой
ExpectedVersionbodylongДа>= 1; сверка с Versionoptimistic concurrency

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

ПолеТипОбязательностьИсточникMaskingОписание
RuleRewardRuleModelДаобновлённый агрегатПроекция после активации (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, но вне периода).

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер компании-застройщика; scope — организация застройщика (TenantId); требуется право rewards.rules.manage.
  2. Проверка входа и предусловий. RuleId непустой, ExpectedVersion >= 1. Правило существует и принадлежит tenant-у застройщика — иначе REWARDS_RULE_NOT_FOUND. Статус правила = Draft (единственный допустимый исходный) — иначе REWARDS_RULE_INVALID_STATUS_TRANSITION. Version совпадает с ExpectedVersion — иначе REWARDS_CONCURRENCY_CONFLICT. Если EffectiveTo задан и уже в прошлом — REWARDS_RULE_RANGE_INVALID (нельзя активировать истёкшее правило).
  3. Проверка инвариантов активации.
    • Уникальность приоритета (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 активна не более одной записи специфичности).
  4. Переход и запись. Правило переходит Draft → Active; EffectiveFrom остаётся null (действует сразу), если не задан; фиксируются актор и новая версия. Переход правила сохраняется в одном согласованном изменении.
  5. События. Публикуется 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.
  6. Результат. Проекция обновлённого правила (RewardRuleModel, Status = Active) → 200.

Диаграмма

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

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

Окно 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_FOUND404Правило не существует / чужой tenant«Правило не найдено»no retry
REWARDS_RULE_INVALID_STATUS_TRANSITION409Status != Draft«Активировать можно только черновик»no retry
REWARDS_RULE_PRIORITY_TAKEN409Приоритет занят активным правилом (INV-3)«Приоритет уже используется действующим правилом»no retry
REWARDS_REWARD_RULE_OVERLAP409Период пересекается с активным правилом той же сигнатуры специфичности (INV-9)«Период пересекается с действующим правилом той же области»no retry (сузить период или деактивировать конфликтующее)
REWARDS_RULE_LAYOUT_NOT_FOUND400ProjectIds/LayoutGroupIds/LayoutIds нет в реплике chessboard застройщика (INV-4)«Ссылка каталога не найдена» + ids в metadataretry после синка chessboard
REWARDS_RULE_RANGE_INVALID400EffectiveTo в прошлом«Период действия уже истёк»no retry
REWARDS_CONCURRENCY_CONFLICT409ExpectedVersion/xmin конфликт«Правило изменено другим пользователем»retry после refresh

Acceptance-критерии

  • Happy path. Дано: правило в Draft, приоритет свободен среди активных, все LayoutIds есть в реплике, ExpectedVersion актуален. Когда: POST .../activate. Тогда: 200, Status = Active, Version + 1; опубликовано RewardRuleChangedKafkaEvent; catalog обновит комиссию в окне секунд.
  • Не черновик. Дано: правило уже Active или Archived. Когда: POST .../activate. Тогда: 409 REWARDS_RULE_INVALID_STATUS_TRANSITION, событие не публикуется.
  • Приоритет занят. Дано: среди активных правил tenant-а уже есть правило с тем же Priority. Когда: POST .../activate. Тогда: 409 REWARDS_RULE_PRIORITY_TAKEN (INV-3); переход не выполнен.
  • Пересечение периодов промо. Дано: уже активно правило с сигнатурой ProjectIds = [ЖК-X] и периодом [T1, T2); активируется второе правило той же сигнатуры (ProjectIds = [ЖК-X]) с периодом, пересекающим [T1, T2). Когда: POST .../activate. Тогда: 409 REWARDS_REWARD_RULE_OVERLAP (INV-9); переход не выполнен (сузить период или деактивировать конфликтующее правило). Правило иной сигнатуры (например без ProjectIds — ставка на застройщика) активируется свободно: специфичности разные, пересечение не проверяется.
  • Ссылка каталога не в реплике. Дано: Conditions.ProjectIds/LayoutGroupIds/LayoutIds содержит id, отсутствующий в реплике справочника chessboard застройщика. Когда: POST .../activate. Тогда: 400 REWARDS_RULE_LAYOUT_NOT_FOUND со списком недостающих ids; повтор допустим после синка chessboard.
  • Истёкший период. Дано: EffectiveTo уже в прошлом. Когда: POST .../activate. Тогда: 400 REWARDS_RULE_RANGE_INVALID.
  • Активно, но ещё не действует. Дано: правило в Draft с EffectiveFrom в будущем, приоритет свободен, LayoutIds в реплике. Когда: POST .../activate. Тогда: 200, Status = Active; приоритет занят среди активных (INV-3), но в подбор комиссии правило попадёт только с наступлением EffectiveFrom — до этого момента catalog и внутренний расчёт rewards комиссию по нему не показывают/не применяют (правило Active, но вне периода).
  • Гонка двух активаций. Дано: два менеджера активируют разные черновики с одинаковым Priority. Когда: одновременный POST .../activate. Тогда: один — 200, другой — 409 REWARDS_RULE_PRIORITY_TAKEN (страховка уникальным индексом активных правил).
  • Конкурентная версия. Дано: ExpectedVersion не совпадает с текущим Version. Когда: POST. Тогда: 409 REWARDS_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; dashboard rewards-overview; runbook — не применяется.

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

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

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