PUT /api/v1/agency/commission-policies/{policyId} — Изменение комиссионной политики (только Draft)
Правка полей политики распределения комиссии агентства в статусе Draft. В статусе Active документ
Rules и Name иммутабельны (INV-4 domain/agency-commission-policy.md):
изменение действующей политики выполняется через новый черновик + активацию,
чтобы сохранить воспроизводимость уже применённого распределения (AppliedPolicyId в начислениях).
Назначение
Руководитель/администратор агентства редактирует черновик комиссионной политики (название, комментарий,
документ правил удержания) до её активации. Owning-контекст — rewards; дом полей и инварианты INV-1..7 —
domain/agency-commission-policy.md. Feature group — agency-policy.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | rewards |
| API area | agency |
| Feature group | agency-policy |
| Статус | draft |
| Документ | docs/06-services/rewards/api/agency/update-agency-policy.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (руководитель/администратор агентства) |
| Auth policy / Permissions | rewards.commission-policy.manage |
| Scope (RBAC) | организация: компания Type ∈ {Agency, Broker, Pin} |
| Record-level | не применяется (политика — одна на агентство, tenant-scope) |
| Tenant isolation | TenantId = CompanyId агентства из claims (tenant-интерсептор); чужая политика невидима → 404 |
| Audit | update: UpdatedByUserId = ActorUserId, Version + 1; суммы/ставки удержания не логируются (Sensitive) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/v1/agency/commission-policies/{policyId} |
| Success status | 200 |
| Idempotency header | не применяется (естественная идемпотентность PUT + ExpectedVersion) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
ActorUserId | identity claims (sub) | Guid | Да | Required | — |
PolicyId | route {policyId} | Guid | Да | Required, непустой | — |
ExpectedVersion | body | long | Да | >= 1; сверка с Version агрегата (optimistic concurrency) | REWARDS_CONCURRENCY_CONFLICT |
Name | body | string | Да | 1..256, не пустое после trim | — |
Description | body | string? | Нет | <= 2000 | — |
Rules | body | AgencyDistributionRulesModel | Да | документ правил удержания — дом create-agency-policy.md; ссылки на пользователей в AgentOverrides валидируются при активации (INV-3), не здесь | — |
PUT — полная замена: клиент присылает все редактируемые поля формы значениями из
list-agency-policies.md (AgencyPolicyModel). Status, EffectiveFrom,
TenantId политики через этот метод не меняются (переходы статусов — activate/archive).
AgencyDistributionRulesModel (вход Rules; проекция AgencyDistributionRulesDocument)
Дом полей и семантика — domain/agency-commission-policy.md
(«Модели JSONB»); ContractVersion наружу не принимается — проставляется сервером (= 1).
| Поле | Тип | Обязательность | Ограничения (валидатор) |
|---|---|---|---|
DefaultRule | AgencyRetentionRuleModel | Да | правило по умолчанию для всех агентов (INV-2) |
AgentOverrides | IReadOnlyList<AgentRetentionOverrideModel> | Нет (пусто = нет персональных условий) | ≤ 1000; AgentUserId уникален в списке (INV-3) |
Вложенные AgencyRetentionRuleModel (Kind Percent/FixedAmount/AgentPercentOfPool/
AgentPercentOfPrice, Value, CurrencyId?) и AgentRetentionOverrideModel — дом
list-agency-policies.md. Ограничения значений (INV-2): процентные Kind
(Percent/AgentPercentOfPool/AgentPercentOfPrice) — 0 <= Value <= 100, CurrencyId = null;
FixedAmount — 0 <= Value <= 100000000, CurrencyId задан (ссылка на валюту справочника, дефолт RUB;
RUB-only — ADR-0048; резолв — через internal resolve справочника,
../../../handbook/api/internal/resolve-references.md).
Модель ответа AgencyCommissionPolicyModel
Полная проекция обновлённого AgencyCommissionPolicyEntity (дом полей —
domain/agency-commission-policy.md); та же проекция, что возвращают
create-agency-policy.md/activate.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | Id | — | Id политики. |
Name | string | Да | Name | — | Название («Стандарт 30/70»). |
Description | string? | Нет | Description | — | Комментарий руководителя. |
Status | string (enum AgencyCommissionPolicyStatus) | Да | Status | — | После правки — всегда Draft. |
EffectiveFrom | DateTimeOffset? | Нет | EffectiveFrom | — | null для черновика (заполняется при Activate). |
Rules | AgencyDistributionRulesModel | Да | Rules (jsonb) | полный документ — только rewards.commission-policy.manage/view | Обновлённый документ удержания. |
CreatedAt | DateTimeOffset | Да | CreatedAt | — | Аудит. |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | — | Момент правки. |
Version | long | Да | Version | — | Новый Version — как ExpectedVersion следующей мутации. |
Алгоритм
- Контекст и доступ. Операцию выполняет руководитель/администратор агентства; scope — организация
(
TenantIdиз claims); требуется правоrewards.commission-policy.manage. - Проверка входа и предусловий.
- формат/диапазоны:
Name1..256,Description <= 2000; значенияRules.DefaultRule/AgentOverrides[].Rule(INV-2: процентный Kind0..100без валюты;FixedAmount >= 0с валютой) — иначеREWARDS_POLICY_RULE_INVALID; уникальностьAgentUserIdвAgentOverrides(INV-3) — иначеREWARDS_POLICY_OVERRIDE_INVALID; - существование: политика с
PolicyIdв tenant-е агентства существует; если нет (или чужой tenant) →REWARDS_POLICY_NOT_FOUND; - допустимость состояния: статус —
Draft; вActiveдокументRulesиNameиммутабельны (INV-4), архив не редактируется →REWARDS_POLICY_ACTIVE_IMMUTABLE; - конкуренция: версия совпадает с
ExpectedVersion; иначеREWARDS_CONCURRENCY_CONFLICT; - существование пользователей-оверрайдов (
AgentOverrides[].AgentUserId) здесь не проверяется — отложено до активации (INV-3), чтобы черновик готовить без синхронной зависимости.
- формат/диапазоны:
- Правка черновика. Полная замена редактируемых полей:
Name,Description, документ правилRules(DefaultRule+AgentOverrides,ContractVersion = 1);Status/EffectiveFrom/TenantIdэтот метод не меняет (переходы статусов —activate/archive). Обновляется версия. Доменные события на правке черновика не поднимаются (AgencyPolicy*Event— толькоCreate/Activate/Archive, дом domain/agency-commission-policy.md). - Согласованность. Обновление политики и версии применяется в одном согласованном изменении.
Интеграционных Kafka-событий нет — политика применяется только внутри
rewards(приConfirmначисления), внешним потребителям изменение не нужно. - Результат. Полная проекция обновлённой политики (
Status = Draft, новая версия) → HTTP200.
Диаграмма
Интеграционная последовательность. Правка черновика — целиком внутри rewards, синхронная, без
Kafka-событий: другие сервисы и потребители изменения не видят. Отредактированный документ Rules
начинает применяться к начислениям только после активации — при Confirm
начисления берётся активная политика (INV-4), а не черновик. До активации агентский прогноз доли
(get-my-commission-summary.md) продолжает считаться по прежней
активной политике. Существование пользователей в AgentOverrides синхронно не проверяется — отложено до
активации (INV-3), чтобы черновик не зависел от синхронного вызова identity.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
REWARDS_POLICY_NOT_FOUND | 404 | Политика не существует / чужой tenant | «Политика не найдена» | no retry |
REWARDS_POLICY_ACTIVE_IMMUTABLE | 409 | Status ∉ {Draft}: правка Active (INV-4 Name/Rules иммутабельны) либо Archived | «Редактировать можно только черновик политики — создайте новый и активируйте» | no retry |
REWARDS_POLICY_RULE_INVALID | 400 | INV-2: процентный Kind (Percent/AgentPercentOfPool/AgentPercentOfPrice) вне 0..100 / FixedAmount < 0 или > 100000000 / CurrencyId не задан или не резолвится при FixedAmount / CurrencyId задан при процентном Kind | «Некорректные параметры распределения» | no retry |
REWARDS_POLICY_OVERRIDE_INVALID | 400 | INV-3: дубль AgentUserId в AgentOverrides (существование пользователей проверяется при активации) | «Некорректный персональный оверрайд» | no retry |
REWARDS_CONCURRENCY_CONFLICT | 409 | конфликт версии (ExpectedVersion) | «Политика изменена другим пользователем» | retry после refresh |
Общие коды (ValidationError, FORBIDDEN, UNAUTHORIZED) — README.md.
Предусловия и постусловия
- Предусловия. Политика существует в tenant-е агентства и находится в статусе
Draft. Актор — руководитель/администратор с правомrewards.commission-policy.manage. Клиент располагает актуальнымVersion(из list-agency-policies.md). - Постусловия (успех). Черновик обновлён:
Name,Description, документRulesзаменены целиком (ContractVersion = 1);StatusостаётсяDraft,EffectiveFrom/TenantIdне тронуты;Version + 1. Интеграционные события не публикуются — изменение не покидаетrewardsдо активации. Существование пользователей вAgentOverridesне проверено (отложено до активации, INV-3).
Acceptance-критерии
- Happy path. Дано: политика в
Draft, актор сrewards.commission-policy.manage, валидныеName(1..256),Rules(INV-2: процентный Kind0..100без валюты /FixedAmount 0..100000000с валютой), уникальныеAgentUserIdвAgentOverrides, корректныйExpectedVersion. Когда:PUT .../{policyId}. Тогда:200,Status = Draft, поля заменены,Version + 1; событий нет. - Смена формы правила. Дано: черновик с
DefaultRule.Kind = Percent; клиент меняет форму на прямую долю агентаAgentPercentOfPool,Value = 65,CurrencyId = null. Когда:PUT. Тогда:200, документRulesзаменён целиком,Version + 1— обе формы делят один пул, результат применения эквивалентен по инвариантуagencyShare + agentShare = пул. - Правка действующей политики. Дано: политика в статусе
Active(илиArchived). Когда:PUT. Тогда:409REWARDS_POLICY_ACTIVE_IMMUTABLE(INV-4:Name/Rulesиммутабельны) — правка выполняется через новый черновик + активацию, чтобы сохранитьAppliedPolicyIdуже применённых начислений. - Некорректное правило удержания. Дано:
Rules.DefaultRule.Kind = Percent,Value = 120(илиFixedAmountбезCurrencyId). Когда:PUT. Тогда:400REWARDS_POLICY_RULE_INVALID(INV-2), политика не изменена. - Дубль оверрайда. Дано:
AgentOverridesсодержит два элемента с однимAgentUserId. Когда:PUT. Тогда:400REWARDS_POLICY_OVERRIDE_INVALID(INV-3). - Оверрайд на несуществующего пользователя. Дано:
AgentOverrides[].AgentUserIdуказывает на ещё не синхронизированного/несуществующего пользователя. Когда:PUT. Тогда:200— существование ссылок отложено до активации (INV-3), черновик готовится без синхронной зависимости. - Конкурентный конфликт. Дано:
ExpectedVersionустарел (политику отредактировали параллельно). Когда:PUT. Тогда:409REWARDS_CONCURRENCY_CONFLICT; клиент перечитывает и повторяет. - Чужой tenant / не найдено. Дано:
PolicyIdиз другого агентства или отсутствует. Когда:PUT. Тогда:404REWARDS_POLICY_NOT_FOUND(fail closed).
Совместимость и наблюдаемость
- Контракт PUT полный: добавление нового optional поля правил требует одновременного обновления формы
редактирования (иначе PUT затрёт значение) — фиксируется в changelog клиента. Смена семантики документа
Rules— черезContractVersion + 1+ migration-reader (дом jsonb). - Logs:
TenantId,ActorUserId,PolicyId,result(ставки удержания не логируются — Sensitive); metrics:rewards_agency_policy_update_total{result}, latency p95; traces: OTel, correlation черезtraceparent; dashboardrewards-overview.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): GET /api/v1/agency/commission-policies — Список комиссионных политик агентства, POST /api/v1/agency/commission-policies/{policyId}/activate — Активация политики агентства, POST /api/v1/agency/commission-policies/{policyId}/archive — Архивирование политики, Rewards API — реестр операций и общие контрактные модели
- Use Cases (1): US-реестр: Владелец агентства (US-OWN-AG)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: domain/agency-commission-policy.md (INV-2/3/4, документ
Rules). - Соседние операции: create-agency-policy.md, list-agency-policies.md, activate-agency-policy.md, archive-agency-policy.md.
- Общий конверт ответа/ошибок, paging, masking: README.md.