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

POST /api/v1/platform/commission-accruals/{accrualId}/adjust — Корректировка суммы начисления (gross)

First-class корректировка суммы CommissionAmount комиссионного начисления ПИН-админом (domain/commission-accrual.md, INV-11, ADR-0067 п.4): новая сумма + обязательная причина. Исходная автосчитанная сумма фиксируется в OriginalCommissionAmount; каждая корректировка оставляет append-only аудит-строку CommissionAccrualAdjustmentEntity (Kind = GrossAmount). В Confirmed распределение пересобирается: строка Platform неизменна, пул агентства пересчитывается и делится по применённой политике.

Назначение

ПИН-админ выравнивает сумму начисления с реальностью (договорённость с застройщиком, расхождение с платёжкой при сверке акта, ошибка данных сделки) до получения денег — вместо отмены начисления целиком. После MoneyReceived gross неизменяем: ошибки лечатся только компенсирующим начислением. Feature group — accrual-adjustments.

Metadata

ПолеЗначение
Сервис/контекстrewards
API areaplatform
Feature groupaccrual-adjustments
Статусdraft
Документdocs/06-services/rewards/api/platform/adjust-commission-accrual.md

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

ПроверкаЗначение
Actor typeUser (ПИН-админ)
Auth policy / Permissionsrewards.accruals.adjust (каталог — permission.md)
Scope (RBAC)платформа
Tenant isolationcross-tenant: начисление принадлежит tenant-у агентства; платформенный scope читает и корректирует вне tenant-изоляции по permission (отдельный путь доступа, domain/commission-accrual.md «Безопасность»)
Auditappend-only строка CommissionAccrualAdjustmentEntity (Kind = GrossAmount, OldAmount/NewAmount, обязательная Reason, ActorUserId); UpdatedByUserId, Version + 1

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/platform/commission-accruals/{accrualId}/adjust
Success status200
Idempotency headerне применяется (повтор с тем же ExpectedVersionREWARDS_CONCURRENCY_CONFLICT; каждая применённая корректировка — отдельная аудит-строка)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claims (sub)GuidДаRequiredactor-stamping (ActorUserId аудит-строки)
AccrualIdroute {accrualId}GuidДаRequired, непустой
NewAmountbodyMoneyModelДаValue > 0, scale 2; CurrencyId = валюте начисления (INV-6)Sensitive
ReasonbodystringДаtrim, 1..1000; пустой/whitespace → REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED (INV-11)не содержит PII (договорённость UI)
ExpectedVersionbodylongДа>= 1; сверка с Version начисления (конкуренты — Confirm/Cancel/включение в акт или поручение)optimistic concurrency

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

Полная проекция начисления — дом README.mdCommissionAccrualModel»). Дельта операции: CommissionAmount = NewAmount; OriginalCommissionAmount заполнено (исходная сумма — фиксируется первой корректировкой и далее не меняется); в Confirmed — пересобранные Distributions (три строки); новая Version.

Алгоритм

  1. Контекст и доступ. Операцию выполняет ПИН-админ в платформенном scope; требуется право rewards.accruals.adjust (не совпадает с правом подтверждения rewards.accruals.confirm застройщика).
  2. Проверка входа и предусловий.
    • формат: AccrualId непустой, NewAmount.Value > 0, Reason непустой после trim (иначе REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED), ExpectedVersion >= 1; иначе ValidationError;
    • существование: начисление с AccrualId существует; иначе REWARDS_ACCRUAL_NOT_FOUND;
    • валюта: NewAmount.CurrencyId = CurrencyId начисления (INV-6); иначе REWARDS_CURRENCY_MISMATCH;
    • конкуренция: версия совпадает с ExpectedVersion; иначе REWARDS_CONCURRENCY_CONFLICT. Начисление читается под блокировкой вместе со строками распределения.
  3. Guard-ы корректировки (INV-11). Все нарушения → REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN:
    • статус — PendingConfirmation или Confirmed; после MoneyReceived gross неизменяем (только компенсирующее начисление), Paid/Cancelled — терминальны;
    • ни одна строка распределения не включена в не-отменённое платёжное поручение (PayoutOrderItemId is null у всех строк);
    • начисление не входит в не-отменённый акт поступления (Draft/Registered, domain/platform-receipt.md) — сначала исключить позицию из Draft-акта, затем корректировать;
    • NewAmount.Value > PlatformFeeAmount.Value — пул агентства строго положителен (PlatformFeeAmount считается от базы цены и при корректировке gross не меняется, INV-10).
  4. Применение корректировки. В одном согласованном изменении:
    • при первой корректировке — OriginalCommissionAmount := CommissionAmount (фиксируется один раз);
    • CommissionAmount := NewAmount;
    • в Confirmed — пересборка распределения (INV-4/5/9/10): строка Platform = PlatformFeeAmount (неизменна), пул = NewAmount − PlatformFeeAmount делится между Agency/Agent по применённой политике (AppliedPolicyId); Σ трёх строк = NewAmount (INV-5); в PendingConfirmation строк ещё нет — пересборка не требуется, распределение построится при подтверждении от актуальной суммы;
    • append аудит-строки CommissionAccrualAdjustmentEntity (Kind = GrossAmount, OldAmount = сумма до корректировки, NewAmount, Reason, ActorUserId);
    • Version + 1. Интеграционные события не публикуются: статус начисления не меняется; скорректированная сумма видна в проекциях rewards сразу (строгая согласованность чтения). Если начисление уже включено в чей-то расчёт вне rewards — расхождение поймает перепроверка при регистрации акта (REWARDS_RECEIPT_ITEM_STALE, register-platform-receipt.md).
  5. Результат. Проекция начисления (CommissionAmount = NewAmount, OriginalCommissionAmount, пересобранные Distributions в Confirmed, новая версия) → HTTP 200.

Диаграмма

Ошибки и failure modes

Общие коды (ValidationError, FORBIDDEN, UNAUTHORIZED, REWARDS_CONCURRENCY_CONFLICT) — дом README.md «Общие ошибки». Специфичные для операции:

КодHTTP statusУсловиеСообщениеRetry
REWARDS_ACCRUAL_NOT_FOUND404Начисление не существует«Начисление не найдено»no retry
REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED400Reason пуст/whitespace (INV-11)«Укажите причину корректировки»no retry
REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN409Статус вне PendingConfirmation/Confirmed; строка в неотменённом поручении; начисление в не-отменённом акте; NewAmount <= PlatformFeeAmount (пул ≤ 0) — INV-11«Корректировка недоступна для этого начисления»no retry
REWARDS_CURRENCY_MISMATCH400NewAmount.CurrencyId ≠ валюте начисления (INV-6)«Валюта корректировки не совпадает с валютой начисления»no retry

Acceptance criteria

  • AC-1 (корректировка в Confirmed с пересборкой). Given начисление Confirmed (CommissionAmount = 100 000, PlatformFeeAmount = 10 000, строки свободны, не в акте, Version = N); When ПИН-админ корректирует до NewAmount = 90 000 с причиной; Then 200: OriginalCommissionAmount = 100 000, CommissionAmount = 90 000; строка Platform = 10 000 (неизменна), пул 80 000 переделён между Agency/Agent по применённой политике, Σ трёх строк = 90 000 (INV-5); создана аудит-строка (GrossAmount, 100 000 → 90 000, Reason, актор); Version = N+1.
  • AC-2 (корректировка в PendingConfirmation). Given начисление PendingConfirmation (строк нет); When корректировка; Then 200 — сумма изменена, аудит-строка создана; распределение построится при подтверждении застройщиком уже от новой суммы.
  • AC-3 (повторная корректировка). Given начисление уже корректировалось (OriginalCommissionAmount заполнен); When вторая корректировка; Then 200OriginalCommissionAmount не перезаписывается, добавлена вторая аудит-строка (OldAmount = сумма после первой корректировки).
  • AC-4 (после получения денег). Given начисление MoneyReceived (или Paid/Cancelled); When корректировка; Then REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN (409) — после гейта gross неизменяем, исправление — компенсирующим начислением (ADR-0067).
  • AC-5 (строка в поручении / начисление в акте). Given Agent-строка включена в неотменённое поручение или начисление входит в Draft/Registered-акт; When корректировка; Then REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN (409) — сначала освободить строку (Reject/Cancel поручения) или исключить позицию из Draft-акта.
  • AC-6 (пул ≤ 0). Given PlatformFeeAmount = 10 000; When NewAmount = 10 000 (или меньше); Then REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN (409) — удержание ПИН не пересчитывается от gross, пул обязан остаться положительным (INV-10/11).
  • AC-7 (пустая причина). Given Reason из пробелов; When корректировка; Then REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED (400) — без причины финансовая корректировка запрещена.
  • AC-8 (гонка с включением в акт). Given параллельные adjust и включение начисления в Draft-акт; When обе операции; Then обе мутируют один агрегат — блокировка и сверка версии пропускают одну; если корректировка успела раньше включения, расхождение суммы позиции поймает регистрация акта (REWARDS_RECEIPT_ITEM_STALE).
  • AC-9 (доступ). Given актор без rewards.accruals.adjust (в т.ч. менеджер застройщика, руководитель агентства); When корректировка; Then FORBIDDEN (403) — застройщик корректирует только в составе подтверждения (../admin/confirm-commission-accrual.md), агентство правит только доли пула.

Совместимость и наблюдаемость

  • Контракт additive-only. Аудит-строки append-only — история корректировок не редактируется и не удаляется; отчётность по корректировкам строится по CommissionAccrualAdjustmentEntity (ix_commission_accrual_adjustments_created_at).
  • Logs: AccrualId, DealId, ActorUserId, факт корректировки без сумм (суммы — Sensitive); metrics: rewards_accrual_adjust_total{result}; traces: OTel, correlation через traceparent; dashboard rewards-overview.
  • НФТ (ADR-0056): мутация одного агрегата (пересборка ≤ 3 строк + аудит-строка); целевая латентность p95 < 300 мс; частота — единичные корректировки в неделю (ручная операция ПИН-админа).

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

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

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