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

UC-RWD-014. ПИН корректирует размер реферального вознаграждения партнёра

Назначение

Описывает first-class корректировку суммы уже начисленного реферального вознаграждения оператором ПИН — вместо отмены и пересоздания начисления. Оператор выравнивает сумму Amount реферального начисления с реальностью (договорённость по партнёрской программе, пересчёт базы, ошибка данных сделки), указывая новую сумму и обязательную причину. Исходная автосчитанная сумма (Amount = FinalPrice × Percent / 100) фиксируется в OriginalAmount при первой корректировке; каждая корректировка оставляет append-only аудит-строку (ReferralAccrualAdjustmentEntity: кто, когда, старое → новое, почему). Гейт: корректировка возможна только пока начисление свободно и не выплачено; после Approved (кошелёк партнёра уже кредитован) дельта компенсируется корректирующей проводкой wallet; после Paid/Cancelled сумма неизменяема. Это не отмена начисления (UC-RWD-009 A5/A6) и не правка ставки на будущее (UC-RWD-008). Связан с контекстом rewards.

Metadata

ПолеЗначение
IDUC-RWD-014
Контекстrewards (см. docs/06-services/rewards/)
Типальтернативный (корректирующая операция реферального контура)
Статусdraft

Актор

ПолеЗначение
РольМенеджер ПИН (корректирует); Партнёр — получатель вознаграждения (видит изменение, не инициирует)
Permissions / scopeЛюбое из rewards.referral-rates.manage / rewards.accruals.adjust; scope — платформа
TenantВне tenant-модели — реферальные начисления платформенные (получатель-партнёр может не иметь компании; процесс ведёт ПИН, ADR-0052)
Record-levelНе применяется для оператора (платформенный реестр); партнёр видит только свои начисления (PartnerUserId), корректировку суммы не выполняет

Предусловие

  • Реферальное начисление существует (создано по завершённой сделке приведённого инвестора, UC-RWD-009).
  • Статус ∈ {Pending, Approved} и начисление свободно (PayoutOrderItemId is null) — из Paid/Cancelled или из включённого в неотменённое реферальное поручение сумма неизменяема.
  • У актора — актуальная Version начисления (optimistic concurrency) и одно из требуемых прав.
  • Причина корректировки сформулирована — без непустой Reason корректировка отклоняется.

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементАдмин-панель ПИН → «Рефералы» → реестр реферальных начислений → карточка → «Скорректировать сумму»

Основной Поток

  1. Менеджер ПИН в реестре реферальных начислений (../api/platform/list-referral-accruals.md) открывает начисление и обнаруживает расхождение суммы с фактической договорённостью. Нажимает «Скорректировать сумму».
  2. Фронт вызывает POST /api/v1/platform/referral-accruals/{accrualId}/adjust с телом { newAmount: MoneyModel (Value > 0, scale 2, CurrencyId = валюте начисления), reason: string (1..1000, непусто после trim), expectedVersion: long } (../api/platform/adjust-referral-accrual.md).
  3. Система проверяет доступ и вход: одно из rewards.referral-rates.manage / rewards.accruals.adjust в платформенном scope; начисление найдено (иначе — REWARDS_REFERRAL_ACCRUAL_NOT_FOUND); newAmount.Value > 0 и Reason непуст (иначе — REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_REASON_REQUIRED); CurrencyId = валюте начисления (INV-RA-2, иначе — REWARDS_CURRENCY_MISMATCH); expectedVersion = Version (иначе — REWARDS_CONCURRENCY_CONFLICT). Начисление читается под блокировкой.
  4. Guard корректировки (INV-RA-6): статус ∈ {Pending, Approved} и начисление свободно (PayoutOrderItemId is null) — все нарушения → REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN.
  5. Система применяет корректировку в одном согласованном изменении: при первой корректировке OriginalAmount := Amount (фиксируется однократно и далее не перезаписывается); Amount := newAmount; создаётся append-only аудит-строка ReferralAccrualAdjustmentEntity (OldAmount = сумма до корректировки, NewAmount, Reason, ActorUserId); Version + 1. Статус и AppliedRate (снапшот ставки) не меняются — скорректированная сумма может расходиться с формулой BaseAmount × Percent, в этом и назначение ручной корректировки.
  6. Компенсация кошелька зависит от статуса на момент корректировки:
    • Pending. Кошелёк ещё не кредитован (кредит происходит только на Approved) — интеграционные события не публикуются; при последующем авто-апруве (регистрация акта, UC-RWD-010) кошелёк кредитуется уже новой суммой.
    • Approved. Кошелёк партнёра уже кредитован исходной суммой — публикуется ReferralAccrualAmountAdjustedKafkaEvent (топик pin.rewards.referral-accruals.v1, key = AccrualId, дельта NewAmount − OldAmount) → wallet применяет корректирующую проводку Correction на счёт ReferralCommission партнёра (ADR-0065, асинхронно — окно секунды); notifications уведомляет партнёра об изменении суммы.
  7. Система возвращает проекцию начисления (Amount = newAmount, заполненный OriginalAmount, новая Version); партнёр в ЛК видит обновлённую сумму, оператор — новую запись в истории корректировок.

Альтернативные Потоки

A1. Корректировка в Pending (до получения денег)

  1. Начисление ещё Pending (родительская комиссия не прошла гейт денег): корректировка меняет Amount и OriginalAmount, добавляет аудит-строку, но кошелёк не затрагивается (кредита ещё не было) — событий нет. При последующем авто-апруве регистрацией акта кошелёк кредитуется уже скорректированной суммой. Завершается.

A2. Повторная корректировка

  1. Начисление уже корректировалось (OriginalAmount заполнен): следующая корректировка не перезаписывает OriginalAmount, OldAmount новой аудит-строки = сумме после предыдущей корректировки. Любое число последовательных корректировок допустимо в рамках guard-ов. Завершается.

A3. Начисление занято реферальным поручением

  1. Начисление Approved включено в неотменённое реферальное поручение (PayoutOrderItemId != null) → REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN (409). Сначала освободить начисление (Reject/Cancel поручения, UC-RWD-009), затем корректировать. Возвращается к основному потоку.

A4. Корректировка после выплаты (Paid) или отмены (Cancelled)

  1. Начисление в терминальном статусе Paid/CancelledREWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN (409): после выплаты финансовая история неизменяема, ошибка лечится вне статусной машины (регламент компенсации — возврат/доплата вне платформы). Завершается.

A5. Гонка с авто-апрувом или включением в поручение

  1. Корректировка Approved-начисления параллельно захвату его реферальным поручением: блокировка и сверка версии пропускают одну операцию; проигравший adjust получает REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN (начисление уже занято) либо REWARDS_CONCURRENCY_CONFLICT. Перечитать состояние и повторить.
  2. Корректировка Pending-начисления параллельно авто-апруву (регистрация акта переводит Pending → Approved): сериализуется по версии; выигравшая операция определяет, кредитуется кошелёк исходной или скорректированной суммой (при проигрыше adjust повторяется уже из Approved → компенсация Correction).

Постусловие

  • Amount начисления равен внесённой корректировке; OriginalAmount хранит исходную автосчитанную сумму (расчёт объясним постфактум). На каждую применённую корректировку — ровно одна append-only аудит-строка (старое → новое, обязательная причина, актор); история не редактируется и не удаляется.
  • Кошелёк партнёра согласован с суммой: при корректировке из Approved баланс ReferralCommission выровнен проводкой Correction (асинхронно, окно секунды); при корректировке из Pending кредит произойдёт уже новой суммой при авто-апруве.
  • Статус и снапшот ставки (AppliedRate) не изменены — корректируется только сумма конкретного вознаграждения.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
403 FORBIDDEN403Нет rewards.referral-rates.manage и rewards.accruals.adjustСкрыть действие
REWARDS_REFERRAL_ACCRUAL_NOT_FOUND404Начисление не существуетВернуться к реестру
REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_REASON_REQUIRED400Reason пуст/whitespace (INV-RA-6)Подсветить поле причины
REWARDS_REFERRAL_ACCRUAL_ADJUSTMENT_FORBIDDEN409Статус вне {Pending, Approved}; начисление в неотменённом поручении (INV-RA-6, A3/A4)Показать порядок разблокировки
REWARDS_CURRENCY_MISMATCH400newAmount.CurrencyId ≠ валюте начисления (INV-RA-2)Исправить вход
REWARDS_CONCURRENCY_CONFLICT409expectedVersionVersion (параллельная мутация, A5)Перечитать и повторить

Доменные События

СобытиеКогда поднимаетсяSubscribers
ReferralAccrualAmountAdjustedEventПрименённая корректировка суммы (шаг 5)in-proc аудит

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
ReferralAccrualAmountAdjustedKafkaEvent.v1pin.rewards.referral-accruals.v1 (key = AccrualId)Только при корректировке из Approved (шаг 6) — OldAmount/NewAmount с CurrencyId + CurrencySnapshotwalletCorrection на дельту кредита ReferralCommission (ADR-0065); notifications — партнёру об изменении суммы

Из Pending события не публикуются (кошелёк ещё не кредитован).

Acceptance Criteria

#КритерийПроверка
AC1Корректировка в Pending: OriginalAmount зафиксирован, Amount заменён, аудит-строка создана, событий нет; при авто-апруве кошелёк кредитуется новой суммойтест 50 000 → 45 000 в Pending
AC2Корректировка в Approved публикует ReferralAccrualAmountAdjustedKafkaEvent; wallet применяет Correction на дельтутест 50 000 → 45 000 в ApprovedCorrection −5 000
AC3Повторная корректировка не перезаписывает OriginalAmount; каждая оставляет свою аудит-строку (OldAmount = значение после предыдущей)две последовательные корректировки → 2 строки аудита
AC4После Paid/Cancelled сумма неизменяема (A4)корректировка терминального начисления → 409
AC5Начисление в неотменённом поручении не корректируется (A3)занять поручением → adjust → 409
AC6Без причины любая корректировка отклонена (400), состояние не измененопустая/пробельная Reason
AC7Валюта корректировки ≠ валюте начисления → 400 (INV-RA-2)смена CurrencyId во входе
AC8Гонка с захватом поручением/авто-апрувом разрешается блокировкой и версией; проигравший получает 409параллельные adjust + захват/апрув
AC9История корректировок append-only, видна в реестре ПИН, упорядочена по временичтение проекции начисления

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

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

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