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

POST /api/v1/developer/commission-accruals/{accrualId}/reject — Отказ застройщика от начисления

Переход начисления PendingConfirmation → Cancelled с CancelReason = DeveloperRejected и обязательным комментарием: застройщик не признаёт обязательство по комиссии (спорная сделка, ошибочное начисление).

Назначение

Менеджер застройщика отклоняет начисление из очереди подтверждения (list-developer-accruals.md). Cross-tenant команда (ADR-0052, решение №2 ../README.md). Дом инвариантов — domain/commission-accrual.md (INV-8, enum CommissionCancelReason). Feature group — developer-accruals.

Metadata

ПолеЗначение
Сервис/контекстrewards
API areadeveloper-admin
Feature groupdeveloper-accruals
Статусdraft
Документdocs/06-services/rewards/api/admin/reject-commission-accrual.md

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

ПроверкаЗначение
Actor typeUser (менеджер компании-застройщика)
Auth policy / Permissionsrewards.accruals.confirm
Scope (RBAC)организация: компания Type = Developer
Record-levelaccrual.DeveloperCompanyId == CompanyId актора → иначе 404
Tenant isolationCross-tenant path (как в confirm-commission-accrual.md)
Auditupdate: CancelledAt, CancelReason, UpdatedByUserId, Version + 1; событие статуса

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/developer/commission-accruals/{accrualId}/reject
Success status200
Idempotency headerне применяется (повтор из CancelledREWARDS_ACCRUAL_INVALID_STATUS_TRANSITION)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DeveloperCompanyIdidentity claims (tenant_id)GuidДаRequiredrecord-level
ActorUserIdidentity claims (sub)GuidДаRequired
AccrualIdroute {accrualId}GuidДаRequired, непустой
ExpectedVersionbodylongДа>= 1optimistic concurrency
ReasonbodystringДа1..1000 — обязательный комментарий отказа (enum DeveloperRejected требует комментарий, дом CommissionCancelReason)не содержит PII по договорённости UI; хранится в audit-логе операции

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаCommissionAccrualEntity.IdId начисления.
Statusstring (enum CommissionAccrualStatus)ДаStatusВсегда Cancelled.
CancelReasonstring (enum CommissionCancelReason)ДаCancelReasonВсегда DeveloperRejected.
CancelledAtDateTimeOffsetДаCancelledAtМомент отказа.
VersionlongДаVersionНовая версия.

Предусловия и постусловия

  • Предусловия. Актор — менеджер застройщика с rewards.accruals.confirm; начисление существует, его DeveloperCompanyId = компания актора и статус = PendingConfirmation; передан обязательный Reason (1..1000) и актуальный ExpectedVersion. Строк распределения ещё нет (в PendingConfirmation, INV-4).
  • Постусловия (успех). Начисление в статусе Cancelled с CancelReason = DeveloperRejected; проставлены CancelledAt, актор, новый Version; комментарий Reason записан в audit-лог операции (в агрегат не добавляется); опубликовано CommissionAccrualStatusChangedKafkaEvent (PendingConfirmation → Cancelled).
  • Постусловия (отказ). Статус и версия начисления не изменились; событие не публикуется.

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер компании-застройщика; scope — организация застройщика (DeveloperCompanyId); требуется право rewards.accruals.confirm. Начисление живёт в tenant-е агентства — cross-tenant доступ по явному DeveloperCompanyId.
  2. Проверка входа и предусловий. AccrualId непустой, ExpectedVersion >= 1, Reason длиной 1..1000 (обязательный комментарий отказа) — иначе ValidationError. Начисление существует (загружается под блокировкой на запись) и его DeveloperCompanyId совпадает с компанией актора — иначе REWARDS_ACCRUAL_NOT_FOUND. Статус = PendingConfirmation (INV-8): отказ застройщика допустим только до подтверждения (из Confirmed обязательство уже признано; отмена подтверждённого — только агентская/ручная ManualCorrection) — иначе REWARDS_ACCRUAL_INVALID_STATUS_TRANSITION. Version совпадает с ExpectedVersion — иначе REWARDS_CONCURRENCY_CONFLICT. Строк распределения в PendingConfirmation нет (INV-4) — освобождать нечего.
  3. Переход и запись. Начисление переходит PendingConfirmation → Cancelled с CancelReason = DeveloperRejected; проставляются CancelledAt, актор, новая версия. Комментарий Reason пишется в audit-лог операции (в агрегате поля нет — дом полей не расширяется).
  4. События. Публикуется CommissionAccrualStatusChangedKafkaEvent (топик pin.rewards.commission-accrual-status-changed.v1, ключ = AccrualId; payload: Id, TenantId, DealId, FromStatus = PendingConfirmation, ToStatus = Cancelled, CancelReason = DeveloperRejected, Version) — потребитель notifications уведомляет агента и руководителя агентства.
  5. Результат. Id, Status = Cancelled, CancelReason = DeveloperRejected, CancelledAt, новый Version200.

Диаграмма

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

ТипДетали
Integration eventsCommissionAccrualStatusChangedKafkaEvent / pin.rewards.commission-accrual-status-changed.v1 / key = AccrualId
External callsnone
Cache / projections / searchnone
Notificationsnotifications по событию: агенту «начисление отклонено застройщиком»

Окно eventual-consistency: переход PendingConfirmation → Cancelled фиксируется атомарно; агентство синхронно (сразу после коммита) перестаёт видеть начисление в очереди ожидающих и в сводке PendingConfirmation. CommissionAccrualStatusChangedKafkaEvent уходит через outbox (ключ = AccrualId, порядок статусов по начислению сохраняется); notifications доставляет агенту уведомление об отказе асинхронно (окно секунды). Распределения не строятся (в PendingConfirmation их нет, INV-4) — освобождать/пересчитывать нечего; взамен отклонённого застройщик может создать ручное начисление (create-manual-accrual.md).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
REWARDS_ACCRUAL_NOT_FOUND404Не существует / чужой DeveloperCompanyId«Начисление не найдено»no retry
REWARDS_ACCRUAL_INVALID_STATUS_TRANSITION409Status != PendingConfirmation«Начисление уже обработано»no retry
REWARDS_CONCURRENCY_CONFLICT409ExpectedVersion/xmin конфликт«Начисление изменено — обновите данные»retry после refresh

Acceptance-критерии

  • Happy path. Дано: начисление PendingConfirmation, DeveloperCompanyId = компания актора, Reason задан, ExpectedVersion актуален. Когда: POST .../reject. Тогда: 200, Status = Cancelled, CancelReason = DeveloperRejected, CancelledAt проставлен; опубликовано CommissionAccrualStatusChangedKafkaEvent; notifications уведомит агента.
  • Пустой комментарий. Дано: Reason пуст или длиннее 1000. Когда: POST .../reject. Тогда: 400 ValidationError (комментарий обязателен для DeveloperRejected).
  • Уже обработано. Дано: начисление в Confirmed/Paid/Cancelled. Когда: POST .../reject. Тогда: 409 REWARDS_ACCRUAL_INVALID_STATUS_TRANSITION (отказ допустим только до подтверждения, INV-8; отмена подтверждённого — только ManualCorrection).
  • Чужой застройщик. Дано: DeveloperCompanyId начисления ≠ компания актора. Когда: POST .../reject. Тогда: 404 REWARDS_ACCRUAL_NOT_FOUND.
  • Гонка подтверждение/отказ. Дано: два менеджера одновременно подтверждают и отклоняют одно начисление. Когда: параллельные confirm и reject. Тогда: побеждает первый захвативший запись, второй получает 409 REWARDS_CONCURRENCY_CONFLICT.

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

  • Контракт additive-only; enum CommissionCancelReason расширяется только additive.
  • Logs: AccrualId, DealId, DeveloperCompanyId, Reason (без сумм); metrics: rewards_accrual_reject_total; traces: OTel; dashboard rewards-overview.

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

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

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