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

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 areaagency
Feature groupagency-policy
Статусdraft
Документdocs/06-services/rewards/api/agency/update-agency-policy.md

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

ПроверкаЗначение
Actor typeUser (руководитель/администратор агентства)
Auth policy / Permissionsrewards.commission-policy.manage
Scope (RBAC)организация: компания Type ∈ {Agency, Broker, Pin}
Record-levelне применяется (политика — одна на агентство, tenant-scope)
Tenant isolationTenantId = CompanyId агентства из claims (tenant-интерсептор); чужая политика невидима → 404
Auditupdate: UpdatedByUserId = ActorUserId, Version + 1; суммы/ставки удержания не логируются (Sensitive)

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/v1/agency/commission-policies/{policyId}
Success status200
Idempotency headerне применяется (естественная идемпотентность PUT + ExpectedVersion)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
ActorUserIdidentity claims (sub)GuidДаRequired
PolicyIdroute {policyId}GuidДаRequired, непустой
ExpectedVersionbodylongДа>= 1; сверка с Version агрегата (optimistic concurrency)REWARDS_CONCURRENCY_CONFLICT
NamebodystringДа1..256, не пустое после trim
Descriptionbodystring?Нет<= 2000
RulesbodyAgencyDistributionRulesModelДадокумент правил удержания — дом 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).

ПолеТипОбязательностьОграничения (валидатор)
DefaultRuleAgencyRetentionRuleModelДаправило по умолчанию для всех агентов (INV-2)
AgentOverridesIReadOnlyList<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; FixedAmount0 <= 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Описание
IdGuidДаIdId политики.
NamestringДаNameНазвание («Стандарт 30/70»).
Descriptionstring?НетDescriptionКомментарий руководителя.
Statusstring (enum AgencyCommissionPolicyStatus)ДаStatusПосле правки — всегда Draft.
EffectiveFromDateTimeOffset?НетEffectiveFromnull для черновика (заполняется при Activate).
RulesAgencyDistributionRulesModelДаRules (jsonb)полный документ — только rewards.commission-policy.manage/viewОбновлённый документ удержания.
CreatedAtDateTimeOffsetДаCreatedAtАудит.
UpdatedAtDateTimeOffsetДаUpdatedAtМомент правки.
VersionlongДаVersionНовый Version — как ExpectedVersion следующей мутации.

Алгоритм

  1. Контекст и доступ. Операцию выполняет руководитель/администратор агентства; scope — организация (TenantId из claims); требуется право rewards.commission-policy.manage.
  2. Проверка входа и предусловий.
    • формат/диапазоны: Name 1..256, Description <= 2000; значения Rules.DefaultRule/ AgentOverrides[].Rule (INV-2: процентный Kind 0..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), чтобы черновик готовить без синхронной зависимости.
  3. Правка черновика. Полная замена редактируемых полей: Name, Description, документ правил Rules (DefaultRule + AgentOverrides, ContractVersion = 1); Status/EffectiveFrom/TenantId этот метод не меняет (переходы статусов — activate/archive). Обновляется версия. Доменные события на правке черновика не поднимаются (AgencyPolicy*Event — только Create/Activate/Archive, дом domain/agency-commission-policy.md).
  4. Согласованность. Обновление политики и версии применяется в одном согласованном изменении. Интеграционных Kafka-событий нет — политика применяется только внутри rewards (при Confirm начисления), внешним потребителям изменение не нужно.
  5. Результат. Полная проекция обновлённой политики (Status = Draft, новая версия) → HTTP 200.

Диаграмма

Интеграционная последовательность. Правка черновика — целиком внутри rewards, синхронная, без Kafka-событий: другие сервисы и потребители изменения не видят. Отредактированный документ Rules начинает применяться к начислениям только после активации — при Confirm начисления берётся активная политика (INV-4), а не черновик. До активации агентский прогноз доли (get-my-commission-summary.md) продолжает считаться по прежней активной политике. Существование пользователей в AgentOverrides синхронно не проверяется — отложено до активации (INV-3), чтобы черновик не зависел от синхронного вызова identity.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
REWARDS_POLICY_NOT_FOUND404Политика не существует / чужой tenant«Политика не найдена»no retry
REWARDS_POLICY_ACTIVE_IMMUTABLE409Status ∉ {Draft}: правка Active (INV-4 Name/Rules иммутабельны) либо Archived«Редактировать можно только черновик политики — создайте новый и активируйте»no retry
REWARDS_POLICY_RULE_INVALID400INV-2: процентный Kind (Percent/AgentPercentOfPool/AgentPercentOfPrice) вне 0..100 / FixedAmount < 0 или > 100000000 / CurrencyId не задан или не резолвится при FixedAmount / CurrencyId задан при процентном Kind«Некорректные параметры распределения»no retry
REWARDS_POLICY_OVERRIDE_INVALID400INV-3: дубль AgentUserId в AgentOverrides (существование пользователей проверяется при активации)«Некорректный персональный оверрайд»no retry
REWARDS_CONCURRENCY_CONFLICT409конфликт версии (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: процентный Kind 0..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. Тогда: 409 REWARDS_POLICY_ACTIVE_IMMUTABLE (INV-4: Name/Rules иммутабельны) — правка выполняется через новый черновик + активацию, чтобы сохранить AppliedPolicyId уже применённых начислений.
  • Некорректное правило удержания. Дано: Rules.DefaultRule.Kind = Percent, Value = 120 (или FixedAmount без CurrencyId). Когда: PUT. Тогда: 400 REWARDS_POLICY_RULE_INVALID (INV-2), политика не изменена.
  • Дубль оверрайда. Дано: AgentOverrides содержит два элемента с одним AgentUserId. Когда: PUT. Тогда: 400 REWARDS_POLICY_OVERRIDE_INVALID (INV-3).
  • Оверрайд на несуществующего пользователя. Дано: AgentOverrides[].AgentUserId указывает на ещё не синхронизированного/несуществующего пользователя. Когда: PUT. Тогда: 200 — существование ссылок отложено до активации (INV-3), черновик готовится без синхронной зависимости.
  • Конкурентный конфликт. Дано: ExpectedVersion устарел (политику отредактировали параллельно). Когда: PUT. Тогда: 409 REWARDS_CONCURRENCY_CONFLICT; клиент перечитывает и повторяет.
  • Чужой tenant / не найдено. Дано: PolicyId из другого агентства или отсутствует. Когда: PUT. Тогда: 404 REWARDS_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; dashboard rewards-overview.

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

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

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