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

UC-RWD-009. Жизненный цикл реферального начисления (связь → начисление → гейт → выплата)

Назначение

Описывает сквозной сценарий реферального вознаграждения: партнёр приводит инвестора (реферальная связь в referral), по завершённой сделке приведённого инвестора rewards создаёт реферальное начисление (Pending), после подтверждения получения денег от застройщика (регистрация акта, UC-RWD-010) начисление автоматически переходит в Approved и кредитует кошелёк партнёра, а оператор ПИН выплачивает вознаграждение реферальным платёжным поручением (Paid). Партнёр видит свои начисления и их статусы в личном кабинете. Связан с контекстом rewards.

Metadata

ПолеЗначение
IDUC-RWD-009
Контекстrewards (см. docs/06-services/rewards/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольSystem (consumers rewards) — создание и авто-апрув; Оператор ПИН — реферальное поручение и ручные отмены; Партнёр — просмотр своих начислений
Permissions / scopeОператор ПИН: rewards.payouts.manage/rewards.payouts.approve (реферальные поручения), rewards.accruals.manage (реестр/отмена рефералок) — платформенный scope. Партнёр: rewards.referral.view-own
TenantНачисление вне tenant-модели (получатель — партнёр-пользователь, может не иметь компании; процесс выплаты ведёт ПИН, ADR-0052); реферальное поручение — tenant компании ПИН
Record-levelПартнёр — только свои начисления (PartnerUserId)

Предусловие

  • У инвестора-покупателя есть активная реферальная связь (../../referral/domain/referral-bond.md); её реплика в rewards наполнена consumer-ом pin.referral.bonds.v1 (../consumers/referral-bond.md).
  • Действует применимая ставка реферального вознаграждения на момент завершения сделки (UC-RWD-008) — иначе поток A3.
  • Событие pin.crm.deal-completed.v1 несёт InvestorUserId, FinalPrice и CompletedAt (../../crm/events/deal-completed.md).

Триггер

ПолеЗначение
ИсточникKafka event
Триггерящий элементConsumer rewards, topic pin.crm.deal-completed.v1 (реферальная ветка); далее — регистрация акта (UC-RWD-010) и операции оператора ПИН

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

  1. Связь. Партнёр приводит инвестора; referral публикует bond-created в pin.referral.bonds.v1 → consumer rewards создаёт строку реплики связей (Status = Active, ../consumers/referral-bond.md) — основание будущих начислений без синхронного запроса в referral.
  2. Начисление. crm завершает сделку → реферальная ветка consumer-а (../consumers/crm-deal-completed.md):
    • по InvestorUserId находится активная связь в реплике (нет — A1);
    • guard самореферала (INV-RA-4): реферер связи ≠ агент сделки (иначе — A2);
    • резолвится ставка на asOf = CompletedAt (UC-RWD-008; нет — A3);
    • считается Amount = FinalPrice × Percent / 100 (banker's, 2 знака, INV-RA-2), снимается снапшот ставки (AppliedRateSnapshot), привязывается родительское комиссионное начисление той же сделки (CommissionAccrualId; родителя нет — A4);
    • создаётся ReferralAccrualEntity в Pending — атомарно с комиссионной веткой, идемпотентно по DealId (INV-RA-1); публикуется ReferralAccrualCreatedKafkaEvent (pin.rewards.referral-accruals.v1) → notifications уведомляет партнёра («вознаграждение начислено, ожидает поступления денег»).
  3. Гейт денег. Финансовый оператор ПИН регистрирует акт поступления средств, в который вошло родительское начисление (UC-RWD-010): атомарно с переходом родителя Confirmed → MoneyReceived рефералка переходит Pending → Approved (INV-RA-3; прямого API-перевода в Approved нет) — публикуется статусное событие в pin.rewards.referral-accruals.v1.
  4. Кредит кошелька. wallet по событию Approved кредитует счёт ReferralCommission партнёра (идемпотентность (ReferralCredit, ReferralAccrualId), ADR-0065) — партнёр видит вознаграждение в балансе (../../wallet/README.md).
  5. Реферальное поручение. Оператор ПИН собирает Approved-рефералки, не включённые в другое поручение, в реферальное платёжное поручение (PayoutOrderKind = ReferralReward, tenant — компания ПИН, позиции — по партнёрам: ../api/platform/create-referral-payout-order.md), утверждает его (../api/platform/approve-referral-payout-order.md) и после фактической выплаты вне платформы (исполнение платежей отложено, решение №9 ТЗ) отмечает выплату (../api/platform/mark-referral-payout-order-paid.md).
  6. Выплата. Каскад MarkPaid (INV-7 поручения): каждая привязанная рефералка переходит Approved → Paid (INV-RA-5) — в одном согласованном изменении; публикуются payout-order-status-changed (Paid, Items[]) и статусные события рефералок. wallet дебетует счета партнёров по позициям (идемпотентность (PayoutDebit, ItemId)); notifications уведомляет партнёров о выплате.
  7. Просмотр. Партнёр в ЛК видит свои начисления и статусы: GET /api/v1/my/referral-accruals (../api/partner/list-my-referral-accruals.md, record-level PartnerUserId; InvestorUserId, BaseAmount и служебные ссылки партнёру не отдаются). Реестр всех начислений — у оператора ПИН (../api/platform/list-referral-accruals.md).

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

A1. Активной связи нет

  1. У инвестора сделки нет активной связи в реплике → реферальное начисление не создаётся (штатный случай — сделка без реферала); комиссионная ветка не затронута. Завершается.

A2. Самореферал (INV-RA-4)

  1. Реферер связи совпадает с агентом сделки → начисление не создаётся: skip + метрика rewards_referral_self_skip_total (самому себе партнёрка не платится; обход через вторую учётку — осознанное ограничение v1, разбор — admin-коррекции связей в referral). Завершается.

A3. Применимой ставки нет

  1. Резолюция ставки на asOf = CompletedAt пуста → начисление не создаётся, warn-метрика rewards_referral_accrual_no_rate_total (UC-RWD-008 A5). После заведения ставки с покрывающим периодом replay события создаёт начисление. Завершается.

A4. Родительского комиссионного начисления нет

  1. Комиссионная ветка завершилась skip-ом (нет агента / нет правила застройщика) → рефералка создаётся с CommissionAccrualId = null и остаётся Pending: без родителя гейт денег непроходим.
  2. Менеджер застройщика создаёт ручное начисление по той же сделке (../api/admin/create-manual-accrual.md) — рефералка привязывается к нему по DealId; дальнейший путь — с шага 3 основного потока.

A5. Отмена сделки

  1. До Approved: consumer pin.crm.deal-cancelled.v1 (../consumers/crm-deal-cancelled.md) отменяет рефералку (Pending → Cancelled, CancelReason = DealCancelled); кошелёк не затронут (кредита ещё не было).
  2. После Approved (акт уже зарегистрирован): отмена допустима, только пока начисление свободно (PayoutOrderItemId is null, INV-RA-5); wallet компенсирует кредит партнёра проводкой Correction по статусному событию. Из Paid переходов нет — возврат денег вне платформы + ручной регламент.

A6. Закрытие связи (bond-closed) и фрод

  1. referral закрывает связь (Replaced при перепривязке / Revoked при отзыве) → реплика помечается неактивной: новые начисления по связи прекращаются; созданные остаются (../../referral/events/bond-closed.md).
  2. При подтверждённом фроде оператор ПИН вручную отменяет начисления связи (CancelReason = BondRevoked, ../api/platform/cancel-referral-accrual.md); если начисление было Approved и кредитовано — кошелёк компенсируется Correction.

A7. Повторная доставка / гонки

  1. Повтор deal-completed упирается в уникальность DealId (INV-RA-1) — идемпотентный skip.
  2. Гонка авто-апрува (каскад акта) и отмены разрешается оптимистической блокировкой: каскад апрувит только Pending-рефералки; отменённое начисление актом не воскрешается.
  3. Гонка отмены и включения в поручение: победитель первым проставляет PayoutOrderItemId, проигравшая отмена получает REWARDS_REFERRAL_ACCRUAL_CANCEL_FORBIDDEN.

Постусловие

  • Реферальное начисление прошло Pending → Approved → Paid (или терминально отменено с обязательной причиной CancelReason); все переходы опубликованы в pin.rewards.referral-accruals.v1.
  • Кошелёк партнёра: кредит ReferralCommission при Approved, дебет при Paid — балансы сходятся с проводками rewards (сверка — commission-settlement-saga.md).
  • Партнёр видит в ЛК актуальный статус и применённый процент (AppliedPercent из снапшота); вознаграждение партнёра — сверх комиссии агентства, платит ПИН (пул агентства не уменьшается).

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

КодHTTPУсловиеПоведение клиента
REWARDS_REFERRAL_ACCRUAL_NOT_FOUND404Начисление не существует (platform/partner API)Вернуться к списку
REWARDS_REFERRAL_ACCRUAL_INVALID_STATUS_TRANSITION409Недопустимый переход, в т.ч. попытка прямого Approve (INV-RA-3)Перечитать начисление
REWARDS_REFERRAL_ACCRUAL_CANCEL_FORBIDDEN409Отмена занятого поручением Approved-начисления / из терминального статуса (INV-RA-5, A7)Сначала освободить начисление (Reject/Cancel поручения)
REWARDS_REFERRAL_ACCRUAL_PAID_ONLY_VIA_PAYOUT409Попытка Paid вне каскада реферального поручения (INV-RA-5)— (guard)
REWARDS_REFERRAL_ACCRUAL_AMOUNT_MISMATCH— (consumer)Сумма разошлась с формулой INV-RA-2DLQ + разбор (дефект расчёта)
REWARDS_CURRENCY_MISMATCH400Валюты Money-полей не совпадают (INV-RA-2)— (guard)
REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE409Рефералка уже занята другим поручением (INV-1 поручения)Пересобрать состав поручения

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

СобытиеКогда поднимаетсяSubscribers
ReferralAccruedEventУспешное создание начисления (шаг 2)in-proc аудит

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

СобытиеTopicКогда публикуетсяConsumers
ReferralAccrualCreatedKafkaEvent.v1pin.rewards.referral-accruals.v1Создание начисления (шаг 2)notifications (уведомление партнёру)
ReferralAccrualStatusChangedKafkaEvent.v1pin.rewards.referral-accruals.v1 (тот же топик, key = id начисления — строгий порядок per-начисление)Каждый переход: Approved (шаг 3), Paid (шаг 6), Cancelled (A5/A6)notifications; wallet (Approved → кредит ReferralCommission; Cancelled после ApprovedCorrection, ADR-0065)
PayoutOrderStatusChangedKafkaEvent.v1pin.rewards.payout-order-status-changed.v1Переходы реферального поручения (шаги 5–6)notifications; wallet (дебет по Paid, Items[])

Consumed: pin.referral.bonds.v1 (шаг 1, A6), pin.crm.deal-completed.v1 (шаг 2), pin.crm.deal-cancelled.v1 (A5).

Acceptance Criteria

#КритерийПроверка
AC1Сделка приведённого инвестора с активной связью и ставкой создаёт рефералку Pending со снапшотом ставки; сумма = FinalPrice × % / 100 (INV-RA-2)интеграционный тест ветки consumer-а
AC2Approved достижим только каскадом регистрации акта родителя (INV-RA-3); прямой перевод отклоняетсяпопытка API-апрува → 409; регистрация акта → Approved
AC3Рефералка без родителя (CommissionAccrualId = null) остаётся Pending; ручное начисление по той же сделке открывает гейт (A4)тест сделки без правила → ручное начисление → акт → Approved
AC4Самореферал не порождает начисление (INV-RA-4, A2)связь с реферером = агентом → skip + метрика
AC5bond-closed прекращает новые начисления, созданные остаются (A6)закрыть связь → новая сделка без начисления, старое начисление живо
AC6Отмена Approved-рефералки компенсирует кредит кошелька (Correction); занятая поручением не отменяется (A5/A7)тест отмены после акта; отмена занятой → 409
AC7Paid — только каскадом реферального поручения; дебет кошелька идемпотентен по (PayoutDebit, ItemId)повторная доставка события Paid → один дебет
AC8Партнёр видит только свои начисления; InvestorUserId/BaseAmount ему не отдаютсятест record-level + состав модели partner-area
AC9Повтор deal-completed не создаёт вторую рефералку (INV-RA-1)двойная доставка + гонка инстансов

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

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

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