POST /api/v1/developer/commission-accruals/{accrualId}/confirm — Подтверждение начисления застройщиком
Переход начисления PendingConfirmation → Confirmed: застройщик признаёт обязательство по комиссии
(«подтверждено / ожидает подтверждения»), опционально скорректировав сумму (INV-11). В момент
подтверждения строится распределение комиссии тремя строками (INV-4/INV-5/INV-9/INV-10
domain/commission-accrual.md): Platform — удержание ПИН по
снапшоту условий платформы (domain/platform-commission-terms.md,
ADR-0066), Agency и Agent — деление пула агентства (CommissionAmount − PlatformFeeAmount) по
активной политике агентства.
Назначение
Менеджер застройщика подтверждает начисление из очереди
(list-developer-accruals.md) — при расхождении с договором передаёт
скорректированную сумму с обязательной причиной (AdjustedAmount + AdjustmentReason, аудит-след
корректировки). После подтверждения начисление ожидает получения денег: доля агента становится доступной
платёжному поручению только после регистрации акта поступления средств (Confirmed → MoneyReceived,
domain/platform-receipt.md, ADR-0067). Cross-tenant команда:
начисление живёт в tenant-е агентства, застройщик действует по своему DeveloperCompanyId (ADR-0052,
решение №2 ../README.md). Feature group — developer-accruals.
| Поле | Значение |
|---|
| Сервис/контекст | rewards |
| API area | developer-admin |
| Feature group | developer-accruals |
| Статус | draft |
| Документ | docs/06-services/rewards/api/admin/confirm-commission-accrual.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (менеджер компании-застройщика) |
| Auth policy / Permissions | rewards.accruals.confirm |
| Scope (RBAC) | организация: компания Type = Developer |
| Record-level | accrual.DeveloperCompanyId == CompanyId актора — иначе 404 (не раскрываем существование чужих начислений) |
| Tenant isolation | Cross-tenant path: загрузка начисления вне tenant-интерсептора; изоляция — permission + record-level проверка DeveloperCompanyId |
| Audit | update: ConfirmedAt, ConfirmedByUserId = ActorUserId, UpdatedByUserId, Version + 1; финансовый переход фиксируется событием статуса; корректировка суммы — обязательная append-only строка CommissionAccrualAdjustmentEntity (Kind = GrossAmount, OldAmount/NewAmount, Reason, актор) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/developer/commission-accruals/{accrualId}/confirm |
| Success status | 200 |
| Idempotency header | не применяется (повтор из Confirmed → REWARDS_ACCRUAL_INVALID_STATUS_TRANSITION) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
DeveloperCompanyId | identity claims (tenant_id) | Guid | Да | Required | record-level |
ActorUserId | identity claims (sub) | Guid | Да | Required | — |
AccrualId | route {accrualId} | Guid | Да | Required, непустой | — |
ExpectedVersion | body | long | Да | >= 1; сверка с Version начисления | optimistic concurrency (конкурент — отмена по deal-cancelled) |
AdjustedAmount | body | MoneyModel? | Нет | если задана — Value > 0, scale 2; CurrencyId = валюте начисления (INV-6); Value > PlatformFeeAmount.Value — пул агентства строго положителен (INV-11) | Sensitive — не логируется |
AdjustmentReason | body | string? | Да при заданном AdjustedAmount | 1..1000, не пусто после trim (INV-11) | — |
Модель ответа ConfirmCommissionAccrualCommandResult
Дельта от CommissionAccrualModel (README.md): застройщику распределение не отдаётся.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | CommissionAccrualEntity.Id | — | Id начисления. |
Status | string (enum CommissionAccrualStatus) | Да | Status | — | Всегда Confirmed. |
ConfirmedAt | DateTimeOffset | Да | ConfirmedAt | — | Момент подтверждения. |
CommissionAmount | MoneyModel | Да | CommissionAmount | — | Итоговая подтверждённая сумма (gross): скорректированная, если корректировка выполнялась, иначе исходная. |
IsAdjusted | bool | Да | computed (OriginalCommissionAmount != null) | — | Признак: сумма корректировалась (в этой команде или ранее ПИН-админом). |
OriginalCommissionAmount | MoneyModel? | Нет (null — не корректировалась) | OriginalCommissionAmount | — | Исходная автосчитанная сумма до корректировок (INV-11). |
Version | long | Да | Version | — | Новая версия. |
Предусловия и постусловия
- Предусловия. Актор — менеджер застройщика с
rewards.accruals.confirm; начисление существует,
его DeveloperCompanyId = компания актора и статус = PendingConfirmation; клиент передаёт актуальный
ExpectedVersion. Снапшот условий платформы (PlatformRetentionPercentagePoints/PlatformFeeAmount)
снят при создании начисления. Активная политика агентства-получателя либо существует, либо отсутствует
(допустимо — fallback INV-6 политики).
- Постусловия (успех). Начисление в статусе
Confirmed; проставлены ConfirmedAt, AppliedPolicyId
(или null при fallback), актор, новый Version. При корректировке: CommissionAmount заменён на
AdjustedAmount, исходная сумма зафиксирована в OriginalCommissionAmount (однократно, при первой
корректировке), создана аудит-строка Kind = GrossAmount с причиной. Созданы ровно три строки
распределения (Platform + Agency + Agent, INV-9), сумма которых равна CommissionAmount (INV-5);
строка Platform = PlatformFeeAmount (не пересчитывается при корректировке gross — считается от базы
цены, INV-10). Выплаты пока недоступны — доля агента откроется поручению после MoneyReceived
(регистрация акта, ADR-0067). Опубликовано CommissionAccrualStatusChangedKafkaEvent
(PendingConfirmation → Confirmed).
- Постусловия (отказ). Статус, сумма, версия, распределение и аудит-история начисления не изменились;
событие не публикуется.
Алгоритм
- Контекст и доступ. Операцию выполняет менеджер компании-застройщика; scope — организация
застройщика (
DeveloperCompanyId); требуется право rewards.accruals.confirm. Начисление живёт
в tenant-е агентства — cross-tenant доступ по явному DeveloperCompanyId.
- Проверка входа и предусловий.
AccrualId непустой, ExpectedVersion >= 1. Если задан
AdjustedAmount — Value > 0, scale 2, и обязателен непустой AdjustmentReason (INV-11) — иначе
REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED. Начисление существует (загружается вне tenant-фильтра
под блокировкой на запись — защита от параллельной отмены consumer-ом deal-cancelled) — иначе
REWARDS_ACCRUAL_NOT_FOUND. Record-level: DeveloperCompanyId начисления совпадает с компанией
актора — иначе REWARDS_ACCRUAL_NOT_FOUND (как not-found, без раскрытия чужих начислений). Статус =
PendingConfirmation (INV-3) — иначе REWARDS_ACCRUAL_INVALID_STATUS_TRANSITION. Version
совпадает с ExpectedVersion — иначе REWARDS_CONCURRENCY_CONFLICT.
- Корректировка суммы (опционально, INV-11). Если задан
AdjustedAmount: CurrencyId совпадает с
валютой начисления (INV-6) — иначе REWARDS_CURRENCY_MISMATCH; AdjustedAmount.Value > PlatformFeeAmount.Value — пул агентства строго положителен, иначе
REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN. Исходная сумма фиксируется в OriginalCommissionAmount
(однократно — при первой корректировке), CommissionAmount заменяется на AdjustedAmount, создаётся
аудит-строка CommissionAccrualAdjustmentEntity (Kind = GrossAmount, OldAmount = прежняя сумма,
NewAmount = AdjustedAmount, Reason = AdjustmentReason, актор). PlatformFeeAmount не
пересчитывается — удержание ПИН считается от базы цены, а не от комиссии (INV-10).
- Активная политика агентства. Читается действующая (
Active) комиссионная политика
агентства-получателя. Отсутствие политики допустимо — fallback INV-6
(domain/agency-commission-policy.md).
- Расчёт распределения и переход. Строится трёхстрочное распределение (INV-4/5/9/10):
- строк распределения ещё нет (INV-4) — иначе
REWARDS_ACCRUAL_DISTRIBUTION_EXISTS;
- строка
Platform: RecipientUserId = null, Amount = PlatformFeeAmount (снапшот условий
платформы, снятый при создании; 0 при fallback без условий — строка создаётся всегда);
- пул агентства =
CommissionAmount − PlatformFeeAmount (по итоговой, возможно скорректированной,
сумме); деление пула — по политике (ResolveRetention, дом
domain/agency-commission-policy.md): персональный
оверрайд по AgentUserId или правило по умолчанию; Percent → удержание агентства = процент от
пула (banker's, scale 2); FixedAmount → валюта удержания совпадает с валютой комиссии (иначе
REWARDS_CURRENCY_MISMATCH), удержание = min(сумма удержания, пул) — fixed больше пула срезается,
доля агента = 0 (задокументированный edge-case); доля агента = пул − удержание агентства;
- без политики (INV-6 NoPolicyFallback): удержание агентства = 0, весь пул — агенту,
AppliedPolicyId = null, AppliedRetentionKind = null (строка Platform — независимо от политики);
- создаются строки
Agency (RecipientUserId = null, AppliedRetentionKind = вид применённого
правила) и Agent (RecipientUserId = AgentUserId);
- сумма трёх строк равна
CommissionAmount (INV-5) — иначе REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID;
- начисление переходит
PendingConfirmation → Confirmed; проставляются AppliedPolicyId,
ConfirmedAt, актор, новая версия.
- Согласованность. Начисление, корректировка (если была) и три строки распределения сохраняются в
одном согласованном изменении. Гонка с consumer-ом
deal-cancelled разрешается блокировкой
начисления на запись и оптимистичной версией.
- События. Публикуется
CommissionAccrualStatusChangedKafkaEvent
(топик pin.rewards.commission-accrual-status-changed.v1, ключ = AccrualId; payload: Id,
TenantId, DealId, FromStatus = PendingConfirmation, ToStatus = Confirmed, ConfirmedAt,
Version) — потребитель notifications уведомляет агента и руководителя агентства.
- Результат.
Id, Status = Confirmed, ConfirmedAt, CommissionAmount (итоговая сумма),
IsAdjusted, OriginalCommissionAmount?, новый Version → 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | CommissionAccrualStatusChangedKafkaEvent / pin.rewards.commission-accrual-status-changed.v1 / key = AccrualId |
| External calls | none |
| Cache / projections / search | none |
| Notifications | notifications по событию: агенту «комиссия подтверждена», руководителю агентства — сводное |
Окно eventual-consistency: начисление, корректировка и три строки распределения фиксируются атомарно;
агентство сразу видит подтверждённую комиссию с раскладкой Platform/Agency/Agent. Доля агента
становится доступной платёжному поручению (create-payout-order.md)
только после MoneyReceived — регистрации акта поступления средств
(domain/platform-receipt.md, ADR-0067).
CommissionAccrualStatusChangedKafkaEvent уходит через outbox (ключ = AccrualId, порядок статусов
сохраняется); notifications доставляет агенту/руководителю уведомление асинхронно (окно секунды).
Синхронный ответ застройщику отдаёт итоговую CommissionAmount и признак корректировки без распределения
(распределение — внутреннее дело агентства и платформы).
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
REWARDS_ACCRUAL_NOT_FOUND | 404 | Начисление не существует или DeveloperCompanyId чужой | «Начисление не найдено» | no retry |
REWARDS_ACCRUAL_INVALID_STATUS_TRANSITION | 409 | Status != PendingConfirmation (INV-3) | «Начисление уже обработано» | no retry |
REWARDS_ACCRUAL_DISTRIBUTION_EXISTS | 409 | Строки распределения уже существуют (INV-4) | «Распределение уже построено» | no retry |
REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID | 500 | Сумма трёх строк ≠ комиссии (INV-5, дефект расчёта) | «Внутренняя ошибка расчёта» | retry после fix |
REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED | 400 | AdjustedAmount задан без непустого AdjustmentReason (INV-11) | «Укажите причину корректировки суммы» | no retry |
REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN | 409 | AdjustedAmount <= PlatformFeeAmount — пул агентства не положителен (INV-11) | «Скорректированная сумма не покрывает удержание платформы» | no retry |
REWARDS_CURRENCY_MISMATCH | 409 | FixedAmount-правило политики или AdjustedAmount в другой валюте (INV-6 VO) | «Валюта не совпадает с валютой комиссии» | no retry |
REWARDS_CONCURRENCY_CONFLICT | 409 | Конфликт версии ExpectedVersion (параллельная отмена) | «Начисление изменено — обновите данные» | retry после refresh |
Acceptance-критерии
- Подтверждение с политикой (Percent). Дано: начисление
PendingConfirmation
(CommissionAmount = 100 000, PlatformFeeAmount = 10 000), у агентства активная политика с
процентным удержанием 30%. Когда: POST .../confirm. Тогда: 200, Status = Confirmed; созданы три
строки: Platform = 10 000, Agency = 27 000 (30% от пула 90 000, banker's, scale 2),
Agent = 63 000; сумма строк = CommissionAmount (INV-5); AppliedPolicyId заполнен.
- Подтверждение с корректировкой суммы. Дано: начисление
PendingConfirmation с
CommissionAmount = 100 000; застройщик передаёт AdjustedAmount = 90 000 и AdjustmentReason.
Когда: POST .../confirm. Тогда: 200; CommissionAmount = 90 000,
OriginalCommissionAmount = 100 000, IsAdjusted = true; создана аудит-строка GrossAmount
(OldAmount = 100 000, NewAmount = 90 000, причина, актор); PlatformFeeAmount не изменился
(INV-10), пул и строки Agency/Agent посчитаны от скорректированной суммы.
- Корректировка без причины. Дано:
AdjustedAmount задан, AdjustmentReason пуст. Когда: POST .../confirm. Тогда: 400 REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED; статус и сумма прежние.
- Корректировка не покрывает удержание ПИН. Дано:
AdjustedAmount <= PlatformFeeAmount. Когда:
POST .../confirm. Тогда: 409 REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN (пул должен быть строго
положителен, INV-11); подтверждение не выполняется.
- Подтверждение без политики (fallback). Дано: у агентства нет активной политики. Когда:
POST .../confirm. Тогда: 200; строка Platform = PlatformFeeAmount, удержание агентства = 0, весь пул —
агенту; AppliedPolicyId = null, AppliedRetentionKind = null (INV-6 политики); инкремент
rewards_accrual_confirm_no_policy_total.
- Fixed больше пула. Дано: политика с
FixedAmount > пула агентства в той же валюте. Когда:
POST .../confirm. Тогда: 200; удержание агентства = пул, доля агента = 0, строка Platform не
затронута (срез — задокументированный edge-case, не ошибка).
- Несовпадение валют. Дано:
FixedAmount-правило политики (или AdjustedAmount) в валюте, отличной
от комиссии. Когда: POST .../confirm. Тогда: 409 REWARDS_CURRENCY_MISMATCH; распределение не
построено, статус прежний.
- Повторное подтверждение. Дано: начисление уже
Confirmed (или MoneyReceived/Paid/Cancelled).
Когда: POST .../confirm. Тогда: 409 REWARDS_ACCRUAL_INVALID_STATUS_TRANSITION; второй набор строк
не создаётся.
- Чужой застройщик. Дано:
DeveloperCompanyId начисления ≠ компания актора. Когда: POST .../confirm. Тогда: 404 REWARDS_ACCRUAL_NOT_FOUND (чужое начисление не раскрывается).
- Гонка с отменой сделки. Дано: параллельно приходит
deal-cancelled по той же сделке. Когда:
одновременные подтверждение и отмена. Тогда: побеждает захвативший запись первым; проигравший получает
409 REWARDS_CONCURRENCY_CONFLICT и перечитывает актуальный статус.
Совместимость и наблюдаемость
- Payload события — additive-only; breaking →
...status-changed.v2. AdjustedAmount/AdjustmentReason —
optional-поля body (additive для клиентов).
- Logs:
AccrualId, DealId, DeveloperCompanyId, FromStatus→ToStatus, IsAdjusted (суммы не
логируются); metrics: rewards_accrual_confirm_total{result}, rewards_accrual_confirm_no_policy_total
(fallback INV-6 политики), rewards_accrual_confirm_adjusted_total (доля подтверждений с
корректировкой); traces: OTel; dashboard rewards-overview; runbook —
runbooks/rewards-accrual-replay.md (по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (12): GET /api/v1/developer/commission-accruals — Начисления комиссий для застройщика, POST /api/v1/agency/agent-boost-campaigns — Создание boost-кампании агентов (черновик), POST /api/v1/agency/agent-boost-campaigns/{campaignId}/activate — Активация boost-кампании, POST /api/v1/agency/agent-boost-campaigns/{campaignId}/archive — Архивирование boost-кампании, POST /api/v1/agency/commission-accruals/{accrualId}/adjust-distribution — Per-deal корректировка долей внутри пула, POST /api/v1/agency/commission-policies — Создание комиссионной политики агентства (черновик), POST /api/v1/agency/commission-policies/{policyId}/activate — Активация политики агентства, POST /api/v1/agency/commission-policies/{policyId}/archive — Архивирование политики, POST /api/v1/developer/commission-accruals — Ручное создание начисления застройщиком, POST /api/v1/developer/commission-accruals/{accrualId}/reject — Отказ застройщика от начисления, POST /api/v1/platform/commission-accruals/{accrualId}/adjust — Корректировка суммы начисления (gross), Rewards API — реестр операций и общие контрактные модели
- Use Cases (2): UC-RWD-004. Застройщик подтверждает начисление комиссии, UC-RWD-011. Корректировки начисления комиссии
- События (1): Event Contract: pin.rewards.commission-accrual-status-changed (CommissionAccrualStatusChangedKafkaEvent)
- Консюмеры (1): Consumer: CRM Deal Completed
- Саги (1): Saga «Commission Settlement» (финансовый контур вознаграждений)
- adr (1): ADR-0067 — гейт получения денег (акт поступления средств)
- registries (1): Реестр API-методов PIN
- index.md (1): Rewards Service
Связанные документы