UC-RWD-009. Жизненный цикл реферального начисления (связь → начисление → гейт → выплата)
Назначение
Описывает сквозной сценарий реферального вознаграждения: партнёр приводит инвестора (реферальная связь в
referral), по завершённой сделке приведённого инвестора rewards создаёт реферальное начисление
(Pending), после подтверждения получения денег от застройщика (регистрация акта, UC-RWD-010)
начисление автоматически переходит в Approved и кредитует кошелёк партнёра, а оператор ПИН выплачивает
вознаграждение реферальным платёжным поручением (Paid). Партнёр видит свои начисления и их статусы в
личном кабинете. Связан с контекстом rewards.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-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) и операции оператора ПИН |
Основной Поток
- Связь. Партнёр приводит инвестора;
referralпубликуетbond-createdвpin.referral.bonds.v1→ consumerrewardsсоздаёт строку реплики связей (Status = Active, ../consumers/referral-bond.md) — основание будущих начислений без синхронного запроса вreferral. - Начисление.
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уведомляет партнёра («вознаграждение начислено, ожидает поступления денег»).
- по
- Гейт денег. Финансовый оператор ПИН регистрирует акт поступления средств, в который вошло
родительское начисление (UC-RWD-010): атомарно с переходом родителя
Confirmed → MoneyReceivedрефералка переходитPending → Approved(INV-RA-3; прямого API-перевода вApprovedнет) — публикуется статусное событие вpin.rewards.referral-accruals.v1. - Кредит кошелька.
walletпо событиюApprovedкредитует счётReferralCommissionпартнёра (идемпотентность(ReferralCredit, ReferralAccrualId), ADR-0065) — партнёр видит вознаграждение в балансе (../../wallet/README.md). - Реферальное поручение. Оператор ПИН собирает
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). - Выплата. Каскад
MarkPaid(INV-7 поручения): каждая привязанная рефералка переходитApproved → Paid(INV-RA-5) — в одном согласованном изменении; публикуютсяpayout-order-status-changed(Paid,Items[]) и статусные события рефералок.walletдебетует счета партнёров по позициям (идемпотентность(PayoutDebit, ItemId));notificationsуведомляет партнёров о выплате. - Просмотр. Партнёр в ЛК видит свои начисления и статусы:
GET /api/v1/my/referral-accruals(../api/partner/list-my-referral-accruals.md, record-levelPartnerUserId;InvestorUserId,BaseAmountи служебные ссылки партнёру не отдаются). Реестр всех начислений — у оператора ПИН (../api/platform/list-referral-accruals.md).
Альтернативные Потоки
A1. Активной связи нет
- У инвестора сделки нет активной связи в реплике → реферальное начисление не создаётся (штатный случай — сделка без реферала); комиссионная ветка не затронута. Завершается.
A2. Самореферал (INV-RA-4)
- Реферер связи совпадает с агентом сделки → начисление не создаётся: skip + метрика
rewards_referral_self_skip_total(самому себе партнёрка не платится; обход через вторую учётку — осознанное ограничение v1, разбор — admin-коррекции связей вreferral). Завершается.
A3. Применимой ставки нет
- Резолюция ставки на
asOf = CompletedAtпуста → начисление не создаётся, warn-метрикаrewards_referral_accrual_no_rate_total(UC-RWD-008 A5). После заведения ставки с покрывающим периодом replay события создаёт начисление. Завершается.
A4. Родительского комиссионного начисления нет
- Комиссионная ветка завершилась skip-ом (нет агента / нет правила застройщика) → рефералка создаётся
с
CommissionAccrualId = nullи остаётсяPending: без родителя гейт денег непроходим. - Менеджер застройщика создаёт ручное начисление по той же сделке
(../api/admin/create-manual-accrual.md) — рефералка
привязывается к нему по
DealId; дальнейший путь — с шага 3 основного потока.
A5. Отмена сделки
- До
Approved: consumerpin.crm.deal-cancelled.v1(../consumers/crm-deal-cancelled.md) отменяет рефералку (Pending → Cancelled,CancelReason = DealCancelled); кошелёк не затронут (кредита ещё не было). - После
Approved(акт уже зарегистрирован): отмена допустима, только пока начисление свободно (PayoutOrderItemId is null, INV-RA-5);walletкомпенсирует кредит партнёра проводкойCorrectionпо статусному событию. ИзPaidпереходов нет — возврат денег вне платформы + ручной регламент.
A6. Закрытие связи (bond-closed) и фрод
referralзакрывает связь (Replacedпри перепривязке /Revokedпри отзыве) → реплика помечается неактивной: новые начисления по связи прекращаются; созданные остаются (../../referral/events/bond-closed.md).- При подтверждённом фроде оператор ПИН вручную отменяет начисления связи
(
CancelReason = BondRevoked, ../api/platform/cancel-referral-accrual.md); если начисление былоApprovedи кредитовано — кошелёк компенсируетсяCorrection.
A7. Повторная доставка / гонки
- Повтор
deal-completedупирается в уникальностьDealId(INV-RA-1) — идемпотентный skip. - Гонка авто-апрува (каскад акта) и отмены разрешается оптимистической блокировкой: каскад апрувит
только
Pending-рефералки; отменённое начисление актом не воскрешается. - Гонка отмены и включения в поручение: победитель первым проставляет
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_FOUND | 404 | Начисление не существует (platform/partner API) | Вернуться к списку |
REWARDS_REFERRAL_ACCRUAL_INVALID_STATUS_TRANSITION | 409 | Недопустимый переход, в т.ч. попытка прямого Approve (INV-RA-3) | Перечитать начисление |
REWARDS_REFERRAL_ACCRUAL_CANCEL_FORBIDDEN | 409 | Отмена занятого поручением Approved-начисления / из терминального статуса (INV-RA-5, A7) | Сначала освободить начисление (Reject/Cancel поручения) |
REWARDS_REFERRAL_ACCRUAL_PAID_ONLY_VIA_PAYOUT | 409 | Попытка Paid вне каскада реферального поручения (INV-RA-5) | — (guard) |
REWARDS_REFERRAL_ACCRUAL_AMOUNT_MISMATCH | — (consumer) | Сумма разошлась с формулой INV-RA-2 | DLQ + разбор (дефект расчёта) |
REWARDS_CURRENCY_MISMATCH | 400 | Валюты Money-полей не совпадают (INV-RA-2) | — (guard) |
REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE | 409 | Рефералка уже занята другим поручением (INV-1 поручения) | Пересобрать состав поручения |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
ReferralAccruedEvent | Успешное создание начисления (шаг 2) | in-proc аудит |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
ReferralAccrualCreatedKafkaEvent.v1 | pin.rewards.referral-accruals.v1 | Создание начисления (шаг 2) | notifications (уведомление партнёру) |
ReferralAccrualStatusChangedKafkaEvent.v1 | pin.rewards.referral-accruals.v1 (тот же топик, key = id начисления — строгий порядок per-начисление) | Каждый переход: Approved (шаг 3), Paid (шаг 6), Cancelled (A5/A6) | notifications; wallet (Approved → кредит ReferralCommission; Cancelled после Approved → Correction, ADR-0065) |
PayoutOrderStatusChangedKafkaEvent.v1 | pin.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-а |
| AC2 | Approved достижим только каскадом регистрации акта родителя (INV-RA-3); прямой перевод отклоняется | попытка API-апрува → 409; регистрация акта → Approved |
| AC3 | Рефералка без родителя (CommissionAccrualId = null) остаётся Pending; ручное начисление по той же сделке открывает гейт (A4) | тест сделки без правила → ручное начисление → акт → Approved |
| AC4 | Самореферал не порождает начисление (INV-RA-4, A2) | связь с реферером = агентом → skip + метрика |
| AC5 | bond-closed прекращает новые начисления, созданные остаются (A6) | закрыть связь → новая сделка без начисления, старое начисление живо |
| AC6 | Отмена Approved-рефералки компенсирует кредит кошелька (Correction); занятая поручением не отменяется (A5/A7) | тест отмены после акта; отмена занятой → 409 |
| AC7 | Paid — только каскадом реферального поручения; дебет кошелька идемпотентен по (PayoutDebit, ItemId) | повторная доставка события Paid → один дебет |
| AC8 | Партнёр видит только свои начисления; InvestorUserId/BaseAmount ему не отдаются | тест record-level + состав модели partner-area |
| AC9 | Повтор deal-completed не создаёт вторую рефералку (INV-RA-1) | двойная доставка + гонка инстансов |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): US-реестр: Реферальный партнёр (US-PRT), US-реестр: Финансовый блок ПИН (US-FIN)
- scenarios (1): UC-OVR-010. От завершённой сделки до выплаты комиссии и реферального вознаграждения
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- Domain entities: ../domain/referral-accrual.md (поля, INV-RA-1..5,
stateDiagram), ../domain/referral-rate-rule.md (резолюция ставки),
../domain/payout-order.md (
ReferralReward), ../domain/platform-receipt.md (каскад авто-апрува). - Consumers: ../consumers/referral-bond.md (реплика связей), ../consumers/crm-deal-completed.md (реферальная ветка), ../consumers/crm-deal-cancelled.md (отмена).
- События: ../events/referral-accrual-created.md, ../events/referral-accrual-status-changed.md, ../events/payout-order-status-changed.md.
- API: ../api/partner/list-my-referral-accruals.md, ../api/platform/list-referral-accruals.md, ../api/platform/cancel-referral-accrual.md, реферальные поручения — ../api/platform/create-referral-payout-order.md и смежные.
- Сквозной процесс: ../sagas/commission-settlement-saga.md; кошелёк — ../../wallet/README.md.
- Связанные UC: UC-RWD-008 (ставки), UC-RWD-010 (гейт денег), UC-RWD-003 (комиссионная ветка того же события).
- ADR: 0066 (реферальные ставки и отдельная сущность), 0067 (гейт
MoneyReceived), 0065 (кошелёк).