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

POST /api/v1/platform/referral-accruals/{accrualId}/adjust — Корректировка суммы реферального начисления

First-class корректировка суммы Amount реферального начисления оператором ПИН (domain/referral-accrual.md, INV-RA-6): новая сумма + обязательная причина. Исходная автосчитанная сумма (Amount = FinalPrice × Percent / 100) фиксируется в OriginalAmount; каждая корректировка оставляет append-only аудит-строку ReferralAccrualAdjustmentEntity (кто, когда, старое→новое, почему). Гейт: только пока начисление свободно и не выплачено — до включения в реферальное поручение; после Approved кошелёк партнёра уже кредитован, поэтому дельта компенсируется корректирующей проводкой wallet.

Назначение

Оператор ПИН выравнивает сумму реферального вознаграждения с реальностью (договорённость по программе, пересчёт базы, ошибка данных сделки) — вместо отмены начисления целиком. Это не способ отменить начисление (отмена — cancel-referral-accrual.md, ManualCorrection) и не правка ставки на будущее (../../domain/referral-rate-rule.md): операция меняет сумму конкретного уже начисленного вознаграждения. После выплаты (Paid) сумма неизменяема — ошибка лечится вне статусной машины (регламент компенсации). Feature group — referral-accruals.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Менеджер ПИН)
Auth policy / Permissionsлюбое из rewards.referral-rates.manage / rewards.accruals.adjust (каталог — permission.md)
Scope (RBAC)платформа
Tenant isolationвне tenant-модели — реферальные начисления платформенные (ADR-0052)
Auditappend-only строка ReferralAccrualAdjustmentEntity (OldAmount/NewAmount, обязательная Reason, ActorUserId); UpdatedAt, Version + 1

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/platform/referral-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-RA-2)Sensitive
ReasonbodystringДаtrim, 1..1000; пустой/whitespace → REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_REASON_REQUIRED (INV-RA-6)не содержит PII (договорённость UI)
ExpectedVersionbodylongДа>= 1; сверка с Version начисления (конкуренты — авто-апрув регистрацией акта, отмена, включение в поручение)optimistic concurrency

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

Полная проекция начисления — дом list-referral-accruals.mdReferralAccrualModel»). Дельта операции: Amount = NewAmount; OriginalAmount заполнено (исходная сумма — фиксируется первой корректировкой и далее не меняется); новая Version. Статус и AppliedRate (снапшот ставки) не меняются — сумма после корректировки может расходиться с формулой BaseAmount × Percent (это и есть назначение ручной корректировки).

Алгоритм

  1. Контекст и доступ. Операцию выполняет Менеджер ПИН в платформенном scope; требуется одно из прав rewards.referral-rates.manage / rewards.accruals.adjust.
  2. Проверка входа и предусловий.
    • формат: AccrualId непустой, NewAmount.Value > 0, Reason непустой после trim (иначе REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_REASON_REQUIRED), ExpectedVersion >= 1; иначе ValidationError;
    • существование: начисление с AccrualId существует; иначе REWARDS_REFERRAL_ACCRUAL_NOT_FOUND;
    • валюта: NewAmount.CurrencyId = CurrencyId начисления (INV-RA-2); иначе REWARDS_CURRENCY_MISMATCH;
    • конкуренция: версия совпадает с ExpectedVersion; иначе REWARDS_CONCURRENCY_CONFLICT. Начисление читается под блокировкой.
  3. Guard корректировки (INV-RA-6). Все нарушения → REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN:
    • статус — Pending или Approved; из Paid/Cancelled сумма неизменяема (терминальные состояния);
    • начисление свободно (PayoutOrderItemId is null) — включённое в поручение освобождается только через Reject/Cancel самого поручения, после чего корректировка возможна.
  4. Применение корректировки. В одном согласованном изменении:
    • при первой корректировке — OriginalAmount := Amount (фиксируется один раз);
    • Amount := NewAmount;
    • append аудит-строки ReferralAccrualAdjustmentEntity (OldAmount = сумма до корректировки, NewAmount, Reason, ActorUserId);
    • Version + 1. Компенсация кошелька зависит от статуса:
    • Pending — кошелёк ещё не кредитован (кредит происходит только на Approved), интеграционные события не публикуются; скорректированная сумма видна в проекциях rewards сразу и, при последующем авто-апруве, кошелёк кредитуется уже новой суммой;
    • Approved — кошелёк партнёра уже кредитован исходной суммой; публикуется ReferralAccrualAmountAdjustedKafkaEvent (топик pin.rewards.referral-accruals.v1, key = Id) — wallet применяет корректирующую проводку (Correction) на дельту NewAmount − OldAmount (ADR-0065, асинхронно — окно секунды); notifications уведомляет партнёра об изменении суммы.
  5. Результат. Проекция начисления (Amount = NewAmount, OriginalAmount, новая версия) → HTTP 200.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsТолько при корректировке из Approved: ReferralAccrualAmountAdjustedKafkaEventpin.rewards.referral-accruals.v1, key = AccrualId (OldAmount/NewAmount c CurrencyId + CurrencySnapshot../../../handbook/domain/snapshots.md); из Pending события нет (кошелёк ещё не кредитован)
External callsnone
Cache / projections / searchnone (проекции rewards строго согласованы)
Notificationsпри Approvednotifications по событию: партнёру «сумма вознаграждения изменена»; wallet компенсирует дельту кредита счёта ReferralCommission (Correction, ADR-0065)

Ошибки и failure modes

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

КодHTTP statusУсловиеСообщениеRetry
REWARDS_REFERRAL_ACCRUAL_NOT_FOUND404Начисление не существует«Реферальное начисление не найдено»no retry
REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_REASON_REQUIRED400Reason пуст/whitespace (INV-RA-6)«Укажите причину корректировки»no retry
REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN409Статус вне Pending/Approved; начисление включено в неотменённое реферальное поручение (PayoutOrderItemId != null) — INV-RA-6«Корректировка недоступна для этого начисления»no retry
REWARDS_CURRENCY_MISMATCH400NewAmount.CurrencyId ≠ валюте начисления (INV-RA-2)«Валюта корректировки не совпадает с валютой начисления»no retry

Acceptance criteria

  • AC-1 (корректировка в Pending). Given начисление Pending (Amount = 50 000, свободно, Version = N); When оператор корректирует до NewAmount = 45 000 с причиной; Then 200: OriginalAmount = 50 000, Amount = 45 000; создана аудит-строка (50 000 → 45 000, Reason, актор); Version = N+1; интеграционных событий нет; при последующем авто-апруве кошелёк кредитуется на 45 000.
  • AC-2 (корректировка в Approved с компенсацией кошелька). Given начисление Approved, свободное (PayoutOrderItemId = null), кошелёк партнёра кредитован на 50 000; When корректировка до NewAmount = 45 000; Then 200: Amount = 45 000, аудит-строка создана; опубликован ReferralAccrualAmountAdjustedKafkaEventwallet применяет Correction на −5 000 (баланс партнёра уменьшается асинхронно, окно секунды).
  • AC-3 (повторная корректировка). Given начисление уже корректировалось (OriginalAmount заполнен); When вторая корректировка; Then 200OriginalAmount не перезаписывается, добавлена вторая аудит-строка (OldAmount = сумма после первой корректировки).
  • AC-4 (после выплаты). Given начисление Paid (или Cancelled); When корректировка; Then REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN (409) — после выплаты сумма неизменяема, исправление — регламентом компенсации вне статусной машины.
  • AC-5 (занято поручением). Given начисление Approved включено в неотменённое реферальное поручение (PayoutOrderItemId != null); When корректировка; Then REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN (409) — сначала освободить начисление (Reject/Cancel поручения).
  • AC-6 (пустая причина). Given Reason из пробелов; When корректировка; Then REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_REASON_REQUIRED (400) — без причины финансовая корректировка запрещена.
  • AC-7 (валюта). Given NewAmount.CurrencyId ≠ валюте начисления; When корректировка; Then REWARDS_CURRENCY_MISMATCH (400).
  • AC-8 (гонка с включением в поручение). Given параллельные adjust (из Approved) и захват начисления реферальным поручением; When обе операции; Then блокировка и сверка версии пропускают одну; проигравший adjust получает REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN (начисление уже в поручении) либо REWARDS_CONCURRENCY_CONFLICT.
  • AC-9 (доступ). Given актор без rewards.referral-rates.manage и без rewards.accruals.adjust; When корректировка; Then FORBIDDEN (403).

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

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

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

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

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