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.
| Поле | Значение |
|---|
| Сервис/контекст | rewards |
| API area | platform |
| Feature group | referral-accruals |
| Статус | draft |
| Документ | docs/06-services/rewards/api/platform/adjust-referral-accrual.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (Менеджер ПИН) |
| Auth policy / Permissions | любое из rewards.referral-rates.manage / rewards.accruals.adjust (каталог — permission.md) |
| Scope (RBAC) | платформа |
| Tenant isolation | вне tenant-модели — реферальные начисления платформенные (ADR-0052) |
| Audit | append-only строка ReferralAccrualAdjustmentEntity (OldAmount/NewAmount, обязательная Reason, ActorUserId); UpdatedAt, Version + 1 |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/platform/referral-accruals/{accrualId}/adjust |
| Success status | 200 |
| Idempotency header | не применяется (повтор с тем же ExpectedVersion → REWARDS_CONCURRENCY_CONFLICT; каждая применённая корректировка — отдельная аудит-строка) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ActorUserId | identity claims (sub) | Guid | Да | Required | actor-stamping (ActorUserId аудит-строки) |
AccrualId | route {accrualId} | Guid | Да | Required, непустой | — |
NewAmount | body | MoneyModel | Да | Value > 0, scale 2; CurrencyId = валюте начисления (INV-RA-2) | Sensitive |
Reason | body | string | Да | trim, 1..1000; пустой/whitespace → REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_REASON_REQUIRED (INV-RA-6) | не содержит PII (договорённость UI) |
ExpectedVersion | body | long | Да | >= 1; сверка с Version начисления (конкуренты — авто-апрув регистрацией акта, отмена, включение в поручение) | optimistic concurrency |
Модель ответа ReferralAccrualModel
Полная проекция начисления — дом list-referral-accruals.md
(«ReferralAccrualModel»). Дельта операции: Amount = NewAmount; OriginalAmount заполнено (исходная
сумма — фиксируется первой корректировкой и далее не меняется); новая Version. Статус и AppliedRate
(снапшот ставки) не меняются — сумма после корректировки может расходиться с формулой BaseAmount × Percent
(это и есть назначение ручной корректировки).
Алгоритм
- Контекст и доступ. Операцию выполняет Менеджер ПИН в платформенном scope; требуется одно из прав
rewards.referral-rates.manage / rewards.accruals.adjust.
- Проверка входа и предусловий.
- формат:
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.
Начисление читается под блокировкой.
- Guard корректировки (INV-RA-6). Все нарушения →
REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN:
- статус —
Pending или Approved; из Paid/Cancelled сумма неизменяема (терминальные состояния);
- начисление свободно (
PayoutOrderItemId is null) — включённое в поручение освобождается только через
Reject/Cancel самого поручения, после чего корректировка возможна.
- Применение корректировки. В одном согласованном изменении:
- при первой корректировке —
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 уведомляет партнёра об изменении суммы.
- Результат. Проекция начисления (
Amount = NewAmount, OriginalAmount, новая версия) → HTTP 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | Только при корректировке из Approved: ReferralAccrualAmountAdjustedKafkaEvent → pin.rewards.referral-accruals.v1, key = AccrualId (OldAmount/NewAmount c CurrencyId + CurrencySnapshot — ../../../handbook/domain/snapshots.md); из Pending события нет (кошелёк ещё не кредитован) |
| External calls | none |
| Cache / projections / search | none (проекции rewards строго согласованы) |
| Notifications | при Approved — notifications по событию: партнёру «сумма вознаграждения изменена»; wallet компенсирует дельту кредита счёта ReferralCommission (Correction, ADR-0065) |
Ошибки и failure modes
Общие коды (ValidationError, FORBIDDEN, UNAUTHORIZED, REWARDS_CONCURRENCY_CONFLICT) — дом
README.md «Общие ошибки». Специфичные для операции:
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
REWARDS_REFERRAL_ACCRUAL_NOT_FOUND | 404 | Начисление не существует | «Реферальное начисление не найдено» | no retry |
REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_REASON_REQUIRED | 400 | Reason пуст/whitespace (INV-RA-6) | «Укажите причину корректировки» | no retry |
REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN | 409 | Статус вне Pending/Approved; начисление включено в неотменённое реферальное поручение (PayoutOrderItemId != null) — INV-RA-6 | «Корректировка недоступна для этого начисления» | no retry |
REWARDS_CURRENCY_MISMATCH | 400 | NewAmount.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, аудит-строка создана; опубликован
ReferralAccrualAmountAdjustedKafkaEvent → wallet применяет Correction на −5 000 (баланс партнёра
уменьшается асинхронно, окно секунды).
- AC-3 (повторная корректировка). Given начисление уже корректировалось (
OriginalAmount
заполнен); When вторая корректировка; Then 200 — OriginalAmount не перезаписывается,
добавлена вторая аудит-строка (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 мс; частота — единичные корректировки (ручная операция ПИН).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы