POST /api/v1/agency/commission-policies — Создание комиссионной политики агентства (черновик)
Создание комиссионной политики агентства в статусе Draft: правило распределения пула по умолчанию и
персональные оверрайды по агентам (матрица «Агенство» 5.2 — «установка размера комиссии в % или
фиксированной суммой, удерживаемой из вознаграждения агента в пользу агентства»). Правило задаётся в одной
из четырёх форм (AgencyRetentionKind): удержание агентства (Percent/FixedAmount — агенту остаётся
остаток пула) или прямая доля агента (AgentPercentOfPool/AgentPercentOfPrice — агентство удерживает
остаток). Черновик не участвует в распределении до активации; дом полей и
инварианты INV-1..7 — domain/agency-commission-policy.md.
Назначение
Руководитель/администратор агентства заводит настройку распределения комиссии между агентом-исполнителем
и агентством. Политика создаётся черновиком, редактируется до активации и не влияет на подтверждение
начислений (Confirm — domain/commission-accrual.md). Owning-контекст —
rewards, feature group — agency-policy, операция op14 реестра README.md.
| Поле | Значение |
|---|
| Сервис/контекст | rewards |
| API area | agency |
| Feature group | agency-policy |
| Статус | draft |
| Документ | docs/06-services/rewards/api/agency/create-agency-policy.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (руководитель/администратор агентства) |
| Auth policy / Permissions | rewards.commission-policy.manage |
| Scope (RBAC) | организация: компания Type ∈ {Agency, Broker, Pin} |
| Record-level | не применяется (создаётся в своём tenant) |
| Tenant isolation | TenantId = CompanyId агентства из claims (не из body), ADR-0052 |
| Audit | create: CreatedByUserId = ActorUserId, Version = 1, доменное событие AgencyPolicyCreatedEvent |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/agency/commission-policies |
| Success status | 201 (+ Location: /api/v1/agency/commission-policies/{id}) |
| Idempotency header | не применяется (несколько черновиков допустимы; INV-1 стережёт только Active) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
TenantId | identity claims (tenant_id) | Guid | Да | Required; компания Type ∈ {Agency, Broker, Pin} | Tenant isolation |
ActorUserId | identity claims (sub) | Guid | Да | Required | — |
Name | body | string | Да | 1..256, не пусто после trim | — |
Description | body | string? | Нет | <= 2000 | — |
Rules | body | AgencyDistributionRulesModel | Да | по модели ниже (INV-2, INV-3) | ставки удержания не логируются |
Модель AgencyDistributionRulesModel (проекция jsonb AgencyDistributionRulesDocument)
Дом полей — domain/agency-commission-policy.md («Модели JSONB»).
ContractVersion наружу не принимается — сервер проставит = 1.
| Поле | Тип | Обязательность | Ограничения |
|---|
DefaultRule | AgencyRetentionRuleModel | Да | правило по умолчанию для всех агентов агентства |
AgentOverrides | IReadOnlyList<AgentRetentionOverrideModel> | Нет (пусто = нет персональных условий) | <= 1000 элементов; AgentUserId уникален в списке (INV-3) |
Модель AgencyRetentionRuleModel
| Поле | Тип | Обязательность | Ограничения |
|---|
Kind | string (enum AgencyRetentionKind) | Да | Percent / FixedAmount (удержание агентства) / AgentPercentOfPool / AgentPercentOfPrice (прямая доля агента) |
Value | decimal | Да | Percent/AgentPercentOfPool/AgentPercentOfPrice: 0..100, scale 2; FixedAmount: 0..100000000, scale 2 (INV-2) |
CurrencyId | short? | Да при Kind = FixedAmount, null при Percent/AgentPercentOfPool/AgentPercentOfPrice (для AgentPercentOfPrice база — конечная цена сделки, валюта берётся из начисления) | ссылка на валюту справочника (дефолт RUB; RUB-only — ADR-0048); резолвится через internal resolve справочника при сохранении (../../../handbook/api/internal/resolve-references.md) |
Модель AgentRetentionOverrideModel
| Поле | Тип | Обязательность | Ограничения |
|---|
AgentUserId | Guid | Да | пользователь компании TenantId; существование/активность не проверяется здесь — отложено до Activate (INV-3) |
Rule | AgencyRetentionRuleModel | Да | те же ограничения значений |
Модель ответа AgencyCommissionPolicyModel
Полная проекция AgencyCommissionPolicyEntity; дом полей — domain/agency-commission-policy.md.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | Id | — | Id созданной политики. |
Name | string | Да | Name | — | Название политики. |
Description | string? | Нет | Description | — | Комментарий руководителя. |
Status | string (enum AgencyCommissionPolicyStatus) | Да | Status | — | Всегда Draft. |
EffectiveFrom | DateTimeOffset? | Нет | EffectiveFrom | — | null до активации. |
Rules | AgencyDistributionRulesModel | Да | Rules (jsonb) | ставки не логируются | Правила распределения. |
CreatedAt | DateTimeOffset | Да | CreatedAt | — | Момент создания. |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | — | Совпадает с CreatedAt. |
Version | long | Да | Version | — | 1 — для последующего ExpectedVersion. |
Алгоритм
- Контекст и доступ. Операцию выполняет руководитель/администратор агентства (тип компании
Agency/Broker/Pin); scope — организация (TenantId из claims); требуется право
rewards.commission-policy.manage. Иначе — FORBIDDEN (INV-7).
- Проверка входа.
- формат/диапазоны:
Name 1..256, Description <= 2000; значения DefaultRule и каждого
AgentOverrides[].Rule (INV-2: Percent/AgentPercentOfPool/AgentPercentOfPrice — 0..100 и
CurrencyId = null; FixedAmount — >= 0 с заданным CurrencyId, резолвящимся в справочнике валют)
— иначе ошибка валидации / REWARDS_POLICY_RULE_INVALID; лимит
AgentOverrides <= 1000 и уникальность AgentUserId (INV-3) — иначе
REWARDS_POLICY_OVERRIDE_INVALID;
- существование пользователей-оверрайдов (
AgentOverrides[].AgentUserId) здесь не проверяется —
отложено до активации (INV-3), чтобы черновик можно было готовить до синхронизации состава агентства;
- единственность активной политики (INV-1) при создании
Draft не проверяется (несколько черновиков
допустимы).
- Создание черновика. Заводится политика:
Status = Draft, EffectiveFrom = null, Version = 1,
CreatedByUserId = актор; документ правил Rules (DefaultRule + AgentOverrides) сохраняется с
ContractVersion = 1. Инварианты INV-2/INV-3 проверяются при сборке.
- Фиксация факта. Создание сопровождается доменным событием
AgencyPolicyCreatedEvent внутри
rewards. Интеграционных Kafka-событий нет — политика применяется только внутри rewards (при
Confirm начисления), внешним потребителям её изменения не нужны
(domain/agency-commission-policy.md, «Доменные и
интеграционные события»).
- Результат. Полная проекция созданной политики (
Status = Draft, Version = 1) → HTTP 201 +
Location.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
REWARDS_POLICY_RULE_INVALID | 400 | Значение правила вне диапазона (процентные Kind вне 0..100) / CurrencyId не задан или не резолвится в справочнике при FixedAmount / CurrencyId задан при процентном Kind (INV-2) | «Некорректные параметры распределения» | no retry |
REWARDS_POLICY_OVERRIDE_INVALID | 400 | Дубликат AgentUserId в AgentOverrides или превышен лимит 1000 (INV-3) | «Некорректный список персональных условий» | no retry |
Общие коды (ValidationError, FORBIDDEN, UNAUTHORIZED, REWARDS_CONCURRENCY_CONFLICT) — README.md.
Acceptance criteria
- AC-1 (создание черновика). Given валидные
Name (1..256) и Rules.DefaultRule в допустимых
границах (INV-2); When руководитель агентства создаёт политику; Then заводится политика Draft,
Version = 1, EffectiveFrom = null, Rules сохранён с ContractVersion = 1; ответ 201 + Location.
Постусловие: политика в распределении не участвует до активации (activate-agency-policy.md),
интеграционных событий нет.
- AC-2 (несколько черновиков). Given у агентства уже есть черновики/активная политика; When
создаётся ещё один черновик; Then он создаётся успешно — INV-1 стережёт единственность только для
Active.
- AC-3 (оверрайд на будущего сотрудника). Given
AgentOverrides[].AgentUserId, которого пока нет в
реплике состава агентства; When создание черновика; Then успех (201) — существование/активность
проверяются только при активации (INV-3), чтобы черновик можно было готовить до синхронизации состава.
- AC-4 (прямая доля агента). Given
DefaultRule.Kind = AgentPercentOfPool (или AgentPercentOfPrice),
Value = 70, CurrencyId = null; When руководитель создаёт политику; Then заводится черновик
Draft, Version = 1, правило сохранено в форме прямой доли агента (агентство удержит остаток пула при
применении).
- AC-5 (некорректное правило распределения). Given
DefaultRule или любой AgentOverrides[].Rule
с Value вне диапазона (процентный Kind вне 0..100 / FixedAmount < 0), либо FixedAmount без
CurrencyId, либо процентный Kind с непустым CurrencyId (INV-2); When создание; Then
REWARDS_POLICY_RULE_INVALID (400).
- AC-6 (некорректный список оверрайдов). Given дубликат
AgentUserId в AgentOverrides или список
длиннее 1000 (INV-3); When создание; Then REWARDS_POLICY_OVERRIDE_INVALID (400).
Совместимость и наблюдаемость
- Новые optional поля правил — additive (та же семантика jsonb-дома); удаление/переименование/смена типа
или семантики — breaking (
ContractVersion + 1 + migration-reader, domain).
- Logs:
TenantId, ActorUserId, PolicyId, счётчик AgentOverrides (ставки/суммы не логируются —
sensitive); metrics: rewards_policy_create_total{result}; traces: OTel, correlation через traceparent;
dashboard rewards-overview; runbook — не применяется.
- НФТ (ADR-0056): создание одного агрегата с jsonb-документом правил (до 1000 оверрайдов); целевая
латентность p95 < 300 мс; частота — редкая (заведение/пересборка политики агентства, back-office).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы