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

POST /api/v1/platform/receipts/{receiptId}/register — Регистрация акта поступления средств

Перевод акта Draft → Registeredединый гейт всех денежных следствий (ADR-0067): после сверки сходимости (Σ позиций = сумме акта, INV-RC-2) атомарно все начисления-позиции переходят Confirmed → MoneyReceived, связанные реферальные начисления — Pending → Approved, публикуются события получения денег. До регистрации никакие выплаты (агентские и реферальные) недоступны; Registered-акт неизменяем и не откатывается (INV-RC-4, domain/platform-receipt.md).

Назначение

Финансовый оператор ПИН сверил черновик акта с фактическим платежом застройщика и фиксирует получение денег: с этого момента доли агентов доступны платёжным поручениям агентств, реферальные вознаграждения — реферальным поручениям, кошельки получателей кредитуются (ADR-0065). Feature group — receipts.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (финансовый оператор ПИН)
Auth policy / Permissionsrewards.receipts.manage (право независимо от прав арбитража сделок CRM, ADR-0067 п.5)
Scope (RBAC)платформа
Tenant isolationвне tenant-модели — платформенная сущность (ADR-0052); каскад затрагивает начисления разных агентств одного застройщика
Auditupdate: RegisteredAt, RegisteredByUserId = ActorUserId, Version + 1; Registered-акт неизменяем — сам себе аудит-след

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/platform/receipts/{receiptId}/register
Success status200
Idempotency headerне применяется — естественная идемпотентность: повторный вызов по уже Registered-акту — no-op, 200 с текущей проекцией; каскад и события не повторяются
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claims (sub)GuidДаRequiredactor-stamping (RegisteredByUserId)
ReceiptIdroute {receiptId}GuidДаRequired, непустой
ExpectedVersionbodylongДа>= 1; сверка с Version акта (конкурент — Cancel/правка состава)optimistic concurrency

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

Полная проекция акта — дом: create-platform-receipt.mdPlatformReceiptModel»). Дельта операции: Status = Registered, заполнен RegisteredAt, ItemsTotal = Amount (сходимость сверена), новая Version. RegisteredByUserId наружу не выводится (внутренний audit-stamp).

Алгоритм

  1. Контекст и доступ. Операцию выполняет финансовый оператор ПИН в платформенном scope; требуется право rewards.receipts.manage.
  2. Проверка входа и предусловий.
    • формат: ReceiptId непустой, ExpectedVersion >= 1; иначе ValidationError;
    • существование: акт с ReceiptId существует; иначе REWARDS_RECEIPT_NOT_FOUND;
    • идемпотентный повтор: если акт уже Registered — операция завершается no-op: 200 с текущей проекцией, каскад и публикации не повторяются (двойного MoneyReceived/Approved не бывает);
    • допустимость состояния: статус — Draft; из Cancelled регистрация невозможна → REWARDS_RECEIPT_INVALID_STATUS_TRANSITION;
    • конкуренция: версия совпадает с ExpectedVersion; иначе REWARDS_CONCURRENCY_CONFLICT. Акт читается под блокировкой вместе с позициями (защита от параллельных Cancel/правки состава).
  3. Перепроверка состава по актуальному состоянию (позиции могли устареть с момента включения). Каждое начисление-позиция читается под блокировкой:
    • Status = Confirmed и DeveloperCompanyId = застройщику акта (INV-RC-1); нарушение (начисление отменено по расторжению сделки, ещё не подтверждено) → REWARDS_RECEIPT_ITEM_INVALID — оператор исключает позицию из состава и корректирует сумму акта;
    • сумма позиции равна текущему CommissionAmount начисления; расхождение (gross скорректирован после включения — adjust-commission-accrual.md) → REWARDS_RECEIPT_ITEM_STALE — оператор пересобирает состав;
    • ItemsCount >= 1.
  4. Сходимость (INV-RC-2). Σ Items[].Amount.Value = Amount.Value акта; иначе REWARDS_RECEIPT_SUM_MISMATCH. Расхождение фактического платежа лечится до регистрации: корректировкой gross начисления, исключением непокрытых позиций (они остаются Confirmed до следующего акта) или исправлением суммы акта.
  5. Атомарный каскад (INV-RC-4). В одном согласованном изменении:
    • акт → Registered (RegisteredAt, RegisteredByUserId = актор, Version + 1);
    • каждое начисление-позиция → Confirmed → MoneyReceived (MoneyReceivedAt = RegisteredAt, PlatformReceiptId = ReceiptId) — domain/commission-accrual.md;
    • каждое реферальное начисление в Pending, чьё родительское начисление вошло в акт, → Pending → Approved (ApprovedAt = RegisteredAt) — domain/referral-accrual.md, INV-RA-3; отменённые рефералки каскад не трогает;
    • атомарно публикуются все события (см. «Побочные эффекты»). Либо всё, либо ничего: частичной регистрации не бывает; наблюдатель не увидит Registered-акт с начислениями ещё в Confirmed.
  6. Результат. Проекция акта (Status = Registered, RegisteredAt, новая версия) → HTTP 200. Постусловие: Agent-строки распределений позиций доступны платёжным поручениям агентств, Approved-рефералки — реферальным поручениям (create-referral-payout-order.md); кошельки получателей кредитуются контекстом wallet асинхронно по событиям (ADR-0065, окно — секунды).

Диаграмма

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

ТипДетали
Integration eventsCommissionMoneyReceivedKafkaEventpin.rewards.commission-money-received.v1, key = id начисления — по одному на каждое начисление акта (payload: DealId, PlatformReceiptId, Number, MoneyReceivedAt, Distributions[] {RecipientType Platform/Agency/Agent, RecipientUserId?, Amount}; деньги — Value + CurrencyId + CurrencySnapshot, ../../../handbook/domain/snapshots.md); контракт — ../../events/commission-money-received.md. CommissionAccrualStatusChangedKafkaEventpin.rewards.commission-accrual-status-changed.v1, key = id начисления (Confirmed → MoneyReceived) — по каждому начислению (../../events/commission-accrual-status-changed.md). ReferralAccrualStatusChangedKafkaEventpin.rewards.referral-accruals.v1, key = id рефералки (Pending → Approved) — по каждой апрувнутой (domain/referral-accrual.md)
External callsnone
Cache / projections / searchnone (сводки ЛК — SQL по PostgreSQL, ADR-0056)
Notificationsnotifications по событиям: агентам/агентствам «деньги по сделке получены — доля доступна к выплате», партнёрам «вознаграждение подтверждено»

Интеграционная последовательность. Каскад внутри rewards синхронен и атомарен — сразу после 200 Agent-строки и Approved-рефералки доступны сборке поручений. Кредиты кошельков (счета AgentCommission/AgencyCommission/PlatformRevenue по Distributions[], ReferralCommission по апруву рефералки — контекст wallet, ADR-0065) и уведомления — асинхронно по событиям (окно — секунды); в этом окне баланс кошелька может ещё не отражать полученные деньги при уже изменённых статусах в rewards.

Ошибки и failure modes

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

КодHTTP statusУсловиеСообщениеRetry
REWARDS_RECEIPT_NOT_FOUND404Акт не существует«Акт не найден»no retry
REWARDS_RECEIPT_INVALID_STATUS_TRANSITION409Регистрация из Cancelled«Акт нельзя зарегистрировать в текущем статусе»no retry
REWARDS_RECEIPT_ITEM_INVALID409Позиция ссылается на начисление не в Confirmed / чужого застройщика (INV-RC-1 при перепроверке)«Позиция акта недействительна — пересоберите состав»no retry (исключить позицию)
REWARDS_RECEIPT_ITEM_STALE409Сумма позиции разошлась с актуальным CommissionAmount (gross скорректирован после включения)«Сумма позиции устарела — пересоберите состав»no retry (пересобрать состав)
REWARDS_RECEIPT_SUM_MISMATCH409Σ позиций ≠ сумме акта (INV-RC-2)«Сумма позиций не сходится с суммой поступления»no retry (выровнять суммы)

Acceptance criteria

  • AC-1 (регистрация с каскадом). Given Draft-акт (Version = N), позиции — Confirmed-начисления застройщика, Σ позиций = Amount, у части начислений есть Pending-рефералки; When оператор регистрирует с ExpectedVersion = N; Then акт → Registered (RegisteredAt, Version = N+1); каждое начисление → MoneyReceived (MoneyReceivedAt, PlatformReceiptId); каждая Pending-рефералка родителей → Approved; опубликованы commission-money-received ×N, commission-accrual-status-changed ×N, реферальные status-changed ×K; ответ 200. Постусловие: Agent-строки и Approved-рефералки доступны поручениям; wallet асинхронно кредитует счета получателей.
  • AC-2 (идемпотентный повтор). Given акт уже Registered; When повторный register (в т.ч. сетевой ретрай клиента); Then 200 с текущей проекцией — no-op: статусы начислений/рефералок не меняются, события повторно не публикуются.
  • AC-3 (расхождение суммы). Given Σ позиций ≠ Amount; When регистрация; Then REWARDS_RECEIPT_SUM_MISMATCH (409) — акт остаётся Draft, каскад не выполняется, события не публикуются.
  • AC-4 (устаревшая позиция). Given после включения позиции gross начисления скорректирован ПИН-админом; When регистрация; Then REWARDS_RECEIPT_ITEM_STALE (409) — оператор пересобирает состав по актуальным суммам.
  • AC-5 (начисление отменено). Given начисление-позиция отменено из Confirmed (расторжение сделки) до регистрации; When регистрация; Then REWARDS_RECEIPT_ITEM_INVALID (409) — оператор исключает позицию и корректирует сумму акта.
  • AC-6 (атомарность). Given акт с N позициями и K рефералками; When регистрация; Then все переходы и все публикации происходят в одном согласованном изменении — промежуточное состояние («акт Registered, а начисление ещё Confirmed») наружу не наблюдается; при любом отказе шага 3–5 акт целиком остаётся Draft.
  • AC-7 (гонка с отменой/правкой состава). Given параллельные register и cancel (или правка состава) одного Draft-акта; When обе операции; Then одна побеждает по блокировке и сверке версии, проигравшая — REWARDS_CONCURRENCY_CONFLICT (409) либо guard статуса.
  • AC-8 (рефералка без родителя не апрувится). Given рефералка сделки, чьё начисление в акт не вошло; When регистрация; Then эта рефералка остаётся Pending — апрув только через акт её родительского начисления (INV-RA-3).
  • AC-9 (доступ). Given актор без rewards.receipts.manage; When регистрация; Then FORBIDDEN (403).

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

  • Payload событий — additive-only; breaking → новая версия топика (dual-publish + sunset). Повторная доставка событий потребителями дедуплицируется по id начисления/рефералки + переходу (конвенция README.md «Согласованность и порядок событий»).
  • Logs: ReceiptId, Number, DeveloperCompanyId, ItemsCount, число апрувнутых рефералок, id начислений (суммы не логируются — Sensitive); metrics: rewards_receipt_register_total{result}, rewards_accrual_awaiting_receipt_max_age_minutes (gauge + алерт — оператор ПИН как узкое место, ADR-0067); traces: OTel, correlation через traceparent; dashboard rewards-overview; runbook — runbooks/rewards-receipt-register.md (по конвенции).
  • НФТ (ADR-0056): каскад — десятки-сотни строк (позиции + начисления + рефералки) в одной транзакции под блокировкой + публикация 2N+K событий; целевая латентность p95 < 1 с; частота — единицы регистраций в неделю на застройщика.

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

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

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