POST /api/v1/platform/receipts — Создание акта поступления средств (черновик)
Создание «Акта поступления средств» в статусе Draft по одному застройщику с подбором позиций —
подтверждённых комиссионных начислений (Status = Confirmed) этого DeveloperCompanyId, ещё не
включённых в другой не-отменённый акт (domain/platform-receipt.md,
INV-RC-1/INV-RC-3). Черновик показывает предпросмотр сходимости Σ позиций против суммы поступления;
денежные следствия наступают только при регистрации (register-platform-receipt.md).
Назначение
Финансовый оператор ПИН получил пакетный платёж комиссии от застройщика (одна платёжка за несколько
сделок) и заводит акт: указывает застройщика, сумму фактического поступления и реквизит платёжного
документа; система подбирает покрываемые начисления — все свободные Confirmed застройщика либо за
период сделок, либо по явному списку. Feature group — receipts.
| Поле | Значение |
|---|
| Сервис/контекст | rewards |
| API area | platform |
| Feature group | receipts |
| Статус | draft |
| Документ | docs/06-services/rewards/api/platform/create-platform-receipt.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (финансовый оператор ПИН) |
| Auth policy / Permissions | rewards.receipts.manage (каталог — permission.md) |
| Scope (RBAC) | платформа |
| Tenant isolation | вне tenant-модели — платформенная сущность (ADR-0052); позиции могут ссылаться на начисления разных агентств одного застройщика |
| Audit | create: CreatedByUserId = ActorUserId, Version = 1; финансовый документ — полный след через Version + actor-stamping |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/platform/receipts |
| Success status | 201 (+ Location: /api/v1/platform/receipts/{id}) |
| Idempotency header | не применяется (повтор создаёт новый черновик; уже занятые начисления во второй акт не попадут — REWARDS_RECEIPT_ACCRUAL_ALREADY_INCLUDED / REWARDS_RECEIPT_EMPTY) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ActorUserId | identity claims (sub) | Guid | Да | Required | actor-stamping |
DeveloperCompanyId | body | Guid | Да | Required, непустой; логическая ссылка на компанию Type = Developer в identity | — |
Amount | body | MoneyModel | Да | Value > 0, scale 2; CurrencyId — ссылка на валюту справочника, RUB-only (ADR-0048) | Sensitive |
PaymentReference | body | string? | Нет | <= 256; номер/дата платёжки застройщика | — |
Comment | body | string? | Нет | <= 2000 | — |
AccrualIds | body | IReadOnlyList<Guid>? | Нет | уникальны, 1..500; явный состав — взаимоисключим с PeriodFrom/PeriodTo | — |
PeriodFrom | body | DateTimeOffset? | Нет | нижняя граница DealCompletedAt (включительно) для автоподбора | — |
PeriodTo | body | DateTimeOffset? | Нет | верхняя граница DealCompletedAt (исключительно); > PeriodFrom, если обе заданы | — |
Полная проекция акта с позициями; дом полей — domain/platform-receipt.md.
Операции области receipts ссылаются на эту модель и описывают только дельту.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | PlatformReceiptEntity.Id | — | Id акта. |
Number | string | Да | Number | — | RC-{yyyy}-{seq} — присвоен при создании. |
DeveloperCompanyId | Guid | Да | DeveloperCompanyId | — | Застройщик-плательщик. |
Amount | MoneyModel | Да | Amount | Sensitive | Сумма фактического поступления. |
ItemsTotal | MoneyModel | Да | вычисление (Σ позиций) | Sensitive | Предпросмотр сходимости: Σ Items[].Amount; в Draft может расходиться с Amount — регистрация расхождение не пропустит (INV-RC-2). |
ItemsCount | int | Да | ItemsCount | — | Число позиций. |
PaymentReference | string? | Нет | PaymentReference | — | Реквизит платёжного документа. |
Comment | string? | Нет | Comment | — | Комментарий оператора. |
Status | string (enum PlatformReceiptStatus) | Да | Status | — | Здесь всегда Draft. |
RegisteredAt | DateTimeOffset? | Нет | RegisteredAt | — | Заполняется при регистрации. |
CancelledAt | DateTimeOffset? | Нет | CancelledAt | — | Заполняется при отмене черновика. |
Items | IReadOnlyList<PlatformReceiptItemModel> | Да | child-коллекция | Sensitive-суммы | Позиции (не пустой). |
CreatedAt | DateTimeOffset | Да | CreatedAt | — | Момент создания. |
Version | long | Да | Version | — | 1; далее — ExpectedVersion мутаций. |
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | PlatformReceiptItemEntity.Id | — | Id позиции. |
CommissionAccrualId | Guid | Да | CommissionAccrualId | — | Покрываемое начисление (domain/commission-accrual.md). |
AgencyCompanyId | Guid | Да | AgencyCompanyId | — | Агентство начисления (денормализация для состава). |
DealId | Guid | Да | начисление | — | Сделка начисления (сверка с платёжкой застройщика). |
Amount | MoneyModel | Да | Amount (= CommissionAmount начисления на момент включения) | Sensitive | Сумма позиции. |
Алгоритм
- Контекст и доступ. Операцию выполняет финансовый оператор ПИН в платформенном scope; требуется
право
rewards.receipts.manage.
- Проверка входа.
DeveloperCompanyId непустой; Amount.Value > 0; AccrualIds (если заданы) —
уникальны, 1..500, и тогда PeriodFrom/PeriodTo не заданы (взаимоисключение); PeriodTo > PeriodFrom,
если обе заданы; длины PaymentReference/Comment; иначе ValidationError.
- Подбор позиций. Кандидаты читаются под блокировкой:
- явный состав (
AccrualIds): каждое начисление существует, Status = Confirmed и его
DeveloperCompanyId равен застройщику акта (INV-RC-1) — иначе REWARDS_RECEIPT_ITEM_INVALID;
не входит в другой не-отменённый акт (Draft/Registered, INV-RC-3) — иначе
REWARDS_RECEIPT_ACCRUAL_ALREADY_INCLUDED;
- автоподбор (без
AccrualIds): все свободные Confirmed-начисления застройщика (не входящие в
не-отменённые акты); при заданном периоде — дополнительно DealCompletedAt ∈ [PeriodFrom, PeriodTo).
Если позиций не набралось ни одной → REWARDS_RECEIPT_EMPTY — пустой черновик не создаётся.
- Валюта.
CurrencyId суммы акта и CommissionAmount всех позиций един — иначе
REWARDS_CURRENCY_MISMATCH (валюта акта = валюте покрываемых начислений).
- Создание черновика. Акт создаётся в
Status = Draft, Version = 1, CreatedByUserId = актор;
номер Number = "RC-{yyyy}-{seq}" — из платформенной последовательности per year; на каждое отобранное
начисление — позиция с Amount = CommissionAmount на момент включения. Акт, позиции и захват начислений
(unique-индекс по CommissionAccrualId, INV-RC-3) фиксируются в одном согласованном изменении; гонка
двух актов за одно начисление разрешается конфликтом индекса — проигравший получает
REWARDS_RECEIPT_ACCRUAL_ALREADY_INCLUDED и пересобирает состав. Статусы начислений на этом шаге не
меняются (переход Confirmed → MoneyReceived — только при регистрации); интеграционные события не
публикуются.
- Результат. Полная проекция акта с позициями и предпросмотром
ItemsTotal против Amount →
HTTP 201 + Location. Расхождение сумм в Draft допустимо: оператор лечит его до регистрации —
правкой состава, корректировкой gross начисления
(adjust-commission-accrual.md) или исправлением Amount акта.
Диаграмма
Ошибки и failure modes
Общие коды (ValidationError, FORBIDDEN, UNAUTHORIZED) — дом README.md «Общие
ошибки», здесь не дублируются. Специфичные для операции:
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
REWARDS_RECEIPT_ITEM_INVALID | 409 | Явно указанное начисление не существует / не Confirmed / чужого застройщика (INV-RC-1) | «Начисление недоступно для включения в акт» | no retry (исправить состав) |
REWARDS_RECEIPT_ACCRUAL_ALREADY_INCLUDED | 409 | Начисление уже в составе другого не-отменённого акта (INV-RC-3; в т.ч. гонка двух создаваемых актов) | «Начисление уже включено в другой акт» | no retry (пересобрать состав) |
REWARDS_RECEIPT_EMPTY | 409 | Ни одной позиции не набралось (нет свободных Confirmed под условия подбора) | «Нет начислений, готовых к включению в акт» | no retry (сменить условия подбора) |
REWARDS_CURRENCY_MISMATCH | 400 | Валюта суммы акта и валюты начислений не совпадают | «Валюта акта не совпадает с валютой начислений» | no retry |
Acceptance criteria
- AC-1 (автоподбор за период). Given у застройщика есть свободные
Confirmed-начисления с
DealCompletedAt в [PeriodFrom, PeriodTo); When оператор создаёт акт с периодом; Then заводится
Draft с Number (RC-{yyyy}-{seq}), на каждое начисление — позиция с Amount = CommissionAmount,
ответ 201 + предпросмотр ItemsTotal; статусы начислений не изменились. Постусловие: включённые
начисления заняты этим актом — второй акт их не подберёт (INV-RC-3).
- AC-2 (явный состав). Given список
AccrualIds из свободных Confirmed-начислений застройщика;
When создание; Then в акт попали ровно перечисленные позиции; лишние начисления автоподбором не
добавляются.
- AC-3 (чужое/неподтверждённое начисление в явном составе). Given в
AccrualIds есть начисление
другого застройщика или в статусе PendingConfirmation/MoneyReceived; When создание; Then
REWARDS_RECEIPT_ITEM_INVALID (409) — акт не создаётся целиком (частичного создания нет).
- AC-4 (занятое начисление). Given начисление уже в
Draft-акте другого оператора; When
создание с ним в составе; Then REWARDS_RECEIPT_ACCRUAL_ALREADY_INCLUDED (409).
- AC-5 (пустой подбор). Given под условия подбора нет ни одного свободного
Confirmed; When
создание; Then REWARDS_RECEIPT_EMPTY (409) — пустой черновик не создаётся.
- AC-6 (расхождение сумм допустимо в Draft). Given Σ позиций ≠
Amount; When создание;
Then 201, ItemsTotal показывает расхождение — блокируется только регистрация (INV-RC-2), не
создание.
- AC-7 (границы периода). Given начисление с
DealCompletedAt == PeriodFrom и начисление с
DealCompletedAt == PeriodTo; When автоподбор; Then первое включено (нижняя граница
включительна), второе — нет (верхняя исключительна).
- AC-8 (конкурентное создание). Given два параллельных акта по одному застройщику; When оба
подбирают позиции; Then каждое свободное начисление попадает ровно в один акт (unique-индекс
INV-RC-3); проигравший явный состав получает
REWARDS_RECEIPT_ACCRUAL_ALREADY_INCLUDED, автоподбор —
оставшиеся свободные либо REWARDS_RECEIPT_EMPTY.
- AC-9 (доступ). Given актор без
rewards.receipts.manage; When создание; Then FORBIDDEN
(403). Агентства и застройщики актов не видят — им виден только эффект по своим начислениям.
Смежные операции (тонкие дельты)
- updatePlatformReceiptItems —
PUT /api/v1/platform/receipts/{receiptId}/items
(rewards.receipts.manage): полная замена состава Draft-акта списком AccrualIds (те же проверки
INV-RC-1/3 и валюты, что на шаге 3–4; исключённые позиции удаляются — начисления освобождаются;
Version + 1). Вне Draft → REWARDS_RECEIPT_COMPOSITION_LOCKED (409); Registered-акт неизменяем →
REWARDS_RECEIPT_REGISTERED_IMMUTABLE (409).
- cancelPlatformReceipt —
POST /api/v1/platform/receipts/{receiptId}/cancel
(rewards.receipts.manage): Draft → Cancelled, позиции физически удаляются — начисления доступны
другому акту (domain/platform-receipt.md, stateDiagram); из
Registered → REWARDS_RECEIPT_REGISTERED_IMMUTABLE (409), повтор из Cancelled →
REWARDS_RECEIPT_INVALID_STATUS_TRANSITION (409). События не публикуются (черновик наружу не
анонсировался).
Совместимость и наблюдаемость
- Контракт additive-only (
PaymentReference/Comment/период — optional); PlatformReceiptModel — дом
этого документа, смежные операции описывают только дельту; breaking-изменение проекции — версия route.
- Logs:
ReceiptId, Number, DeveloperCompanyId, ItemsCount, id начислений (суммы не логируются —
Sensitive); metrics: rewards_receipt_create_total{result}, rewards_receipt_items_count
(гистограмма); traces: OTel, correlation через traceparent; dashboard rewards-overview.
- НФТ (ADR-0056): подбор — по
ix_commission_accruals_developer (developer_company_id, status); объём —
десятки-сотни начислений на застройщика; целевая латентность p95 < 500 мс; частота — единицы актов в
неделю на застройщика (back-office финансового оператора).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы