POST /api/v1/reward-rules — Создание правила вознаграждения (черновик)
Создание нового правила вознаграждения застройщика в статусе Draft: база расчёта (базовая/конечная
цена), процент, приоритет, период действия (EffectiveFrom/EffectiveTo — временные промо-ставки),
условия-предикаты (тип объекта / ProjectIds (ЖК) / LayoutGroupIds / LayoutIds / комнаты /
диапазон площади).
Назначение
Менеджер застройщика настраивает правило комиссии агентов. Правило создаётся черновиком и не влияет на
расчёты до активации. Owning-контекст — rewards, дом полей и инварианты
INV-1..9 — domain/reward-rule.md. Feature group — rules.
| Поле | Значение |
|---|
| Сервис/контекст | rewards |
| API area | developer-admin |
| Feature group | rules |
| Статус | draft |
| Документ | docs/06-services/rewards/api/admin/create-reward-rule.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (менеджер компании-застройщика) |
| Auth policy / Permissions | rewards.rules.manage |
| Scope (RBAC) | организация: компания Type = Developer |
| Record-level | не применяется |
| Tenant isolation | TenantId = CompanyId из claims (не из body) — правило создаётся только в своей компании |
| Audit | create: CreatedByUserId = ActorUserId, Version = 1, доменное событие RewardRuleCreatedEvent |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/reward-rules |
| Success status | 201 (+ Location: /api/v1/reward-rules/{id}) |
| Idempotency header | Idempotency-Key (Guid) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
ActorUserId | identity claims (sub) | Guid | Да | Required | — |
IdempotencyKey | header Idempotency-Key | Guid | Да | Required | — |
Name | body | string | Да | 1..256 | — |
Description | body | string? | Нет | <= 2000 | — |
CalculationBase | body | CommissionCalculationBase | Да | enum BasePrice/FinalPrice | — |
CommissionPercent | body | decimal | Да | > 0, <= 100, scale 2 (INV-1) | — |
Priority | body | int | Да | 0..100000; уникальность среди активных проверяется при активации (INV-3), в Draft дубли допустимы | — |
EffectiveFrom | body | DateTimeOffset? | Нет | — | — |
EffectiveTo | body | DateTimeOffset? | Нет | > EffectiveFrom, если обе заданы (INV-2) | — |
Conditions | body | RewardRuleConditionsModel | Да (все поля могут быть пустыми = правило-default) | по таблице модели (README.md): уникальность и лимиты списков, RoomsRange.Min <= RoomsRange.Max, AreaRangeM2.Min <= AreaRangeM2.Max (INV-2); существование ProjectIds/LayoutGroupIds/LayoutIds в реплике справочника не проверяется здесь — отложено до активации (INV-4), чтобы черновик можно было готовить до синка chessboard | — |
Модель ответа CreateRewardRuleCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | RewardRuleEntity.Id | — | Id созданного правила. |
Status | string (enum RewardRuleStatus) | Да | Status | — | Всегда Draft. |
Version | long | Да | Version | — | 1 — для последующего ExpectedVersion. |
Предусловия и постусловия
- Предусловия. Актор авторизован; его компания — застройщик (
Type = Developer); есть право
rewards.rules.manage. Синхронизация реплики планировок не требуется (черновик готовится заранее).
- Постусловия (успех). В tenant-е застройщика появилось ровно одно новое правило в статусе
Draft
с Version = 1 и зафиксированным автором; правило не участвует в подборе комиссии и не видно
catalog (интеграционное событие не публикуется до активации); приоритет-дубли в Draft допустимы.
- Постусловия (отказ). Ни одно правило не создано; повтор с тем же
Idempotency-Key после успеха
возвращает исходный результат и второго правила не создаёт.
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер компании-застройщика (тип компании
Developer,
иначе FORBIDDEN); scope — организация застройщика (TenantId); требуется право
rewards.rules.manage. Повторный вызов с тем же IdempotencyKey возвращает ранее сохранённый
результат.
- Проверка входа. Проценты (INV-1), приоритет, согласованность диапазонов
Rooms/Area/
Effective (INV-2), лимиты и уникальность элементов списков условий — по таблице входных данных;
нарушение → REWARDS_RULE_PERCENT_OUT_OF_RANGE / REWARDS_RULE_RANGE_INVALID / ValidationError.
Существование ProjectIds/LayoutGroupIds/LayoutIds здесь не проверяется — отложено до активации
(INV-4); непересечение периодов правил одной специфичности (INV-9) — тоже (черновик в подборе не
участвует), проверяется при активации.
- Создание черновика. Формируется правило в статусе
Draft: TenantId из claims; Name,
Description, CalculationBase, CommissionPercent, Priority, EffectiveFrom/To, Conditions
— из тела; фиксируются актор, CreatedAt, Version = 1.
- Согласованность. Правило и запись идемпотентности сохраняются в одном согласованном изменении.
Интеграционного события нет:
RewardRuleChangedKafkaEvent публикуется только при активации,
деактивации, архивации или смене EffectiveTo (черновик не виден catalog).
- Результат.
Id, Status = Draft, Version = 1 (для последующего ExpectedVersion) → 201
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
REWARDS_RULE_PERCENT_OUT_OF_RANGE | 400 | CommissionPercent вне (0;100] (INV-1) | «Процент вне допустимого диапазона» | no retry |
REWARDS_RULE_RANGE_INVALID | 400 | Несогласованные диапазоны комнат/площади/периода (INV-2) | «Некорректный диапазон условий» | no retry |
Общие коды — README.md.
Acceptance-критерии
- Happy path. Дано: менеджер застройщика с
rewards.rules.manage, валидное тело (CommissionPercent
∈ (0;100], согласованные диапазоны). Когда: POST /api/v1/reward-rules. Тогда: 201 + Location,
Status = Draft, Version = 1; правило видно в list-reward-rules.md со
Status = Draft; catalog о черновике не уведомляется.
- Процент вне диапазона. Дано:
CommissionPercent = 0 или > 100. Когда: POST. Тогда: 400
REWARDS_RULE_PERCENT_OUT_OF_RANGE, правило не создано.
- Несогласованный диапазон. Дано:
RoomsRange.Min > RoomsRange.Max (или EffectiveTo <= EffectiveFrom). Когда: POST. Тогда: 400 REWARDS_RULE_RANGE_INVALID.
- Промо-период на черновике. Дано:
Conditions.ProjectIds = [ЖК-X], CommissionPercent = 12,
EffectiveFrom/EffectiveTo на границы недели акции. Когда: POST. Тогда: 201, Status = Draft
(более специфичная промо-ставка на ЖК готовится черновиком; конфликт периодов с другими правилами той
же специфичности всплывёт лишь при активации — INV-9).
- Ссылки каталога не проверяются на черновике. Дано:
Conditions.ProjectIds/LayoutGroupIds/
LayoutIds содержат ещё не синхронизированные из chessboard id. Когда: POST. Тогда: 201
(существование ссылок откладывается до активации — INV-4).
- Идемпотентный повтор. Дано: правило уже создано с ключом
K. Когда: повторный POST с тем же
Idempotency-Key = K. Тогда: тот же Id/Status/Version, второго правила нет.
- Не застройщик / нет права. Дано: компания актора не
Developer либо нет rewards.rules.manage.
Когда: POST. Тогда: 403 FORBIDDEN.
Совместимость и наблюдаемость
- Новые optional измерения
Conditions — additive (та же семантика «null = any», как в jsonb-доме).
- Logs:
TenantId, ActorUserId, RuleId, Priority (процент и условия не логируются — конкурентная
информация застройщика); metrics: rewards_rule_create_total{result}; traces: OTel; dashboard
rewards-overview; runbook — не применяется.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы