POST /api/developer/crm/deals/{dealId}/documents/{documentId}/confirm — Подтвердить документы ДДУ/ДКП
Менеджер застройщика принимает или отклоняет загруженный клиентом документ для формирования договора
ДДУ/ДКП (US-INV-14). Документ переходит UnderReview → Accepted | Rejected (INV-DOC-2). Операция пишет
таймлайн-событие сделки и публикует DocumentStatusChangedEvent. Дом полей —
deal-document.md.
Назначение
Даёт менеджеру застройщика по документу вкладки «Документы» карточки сделки принять решение проверки: принять
(клиент может двигаться дальше по договору) либо отклонить с причиной (клиент видит причину и перезагружает
документ новой записью, INV-DOC-4). На выходе — обновлённый документ. Область — документы, проверяемые
застройщиком (в первую очередь ContractDraftDocument для ДДУ/ДКП; механизм общий для reviewable-типов, где
получатель — застройщик).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | developer |
| Feature group | deal-documents |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/confirm-contract-documents.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика) |
| Auth policy / Permissions | crm.deal-documents.review (JWT identity) |
| Scope (RBAC) | организация (застройщик) — TenantId = DeveloperCompanyId |
| Record-level | актор — сторона-проверяющий типа (INV-DOC-2): загрузивший клиент/агент (investor-area) отклоняется |
| Tenant isolation | TenantId в каждом запросе (= DeveloperCompanyId) |
| Audit | update (решение проверки — иммутабельный факт): RejectReason в логи целиком не пишется |
Решение принимает менеджер застройщика — сторона-проверяющий типа документа (INV-DOC-2). Для типов, где
застройщик сам выступает загрузчиком (BookingContract/SaleContract/RosreestrDocument), проверка
запрещена — CRM_DOCUMENT_REVIEWER_FORBIDDEN. Tenant — TenantId = DeveloperCompanyId в каждом запросе.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/deals/{dealId}/documents/{documentId}/confirm |
| Success status | 200 (решение применено, возвращён обновлённый документ) |
| Idempotency header | не применяется (повтор на терминальном статусе → CRM_DOCUMENT_ALREADY_REVIEWED, шаг 2) |
| Correlation | сквозной trace через заголовок traceparent |
Отдельный idempotency-заголовок не требуется: повторный вызов на документе, уже переведённом в терминальный
статус, безопасно завершается 409 CRM_DOCUMENT_ALREADY_REVIEWED (шаг 2 алгоритма) — без побочных эффектов.
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required (DeveloperCompanyId) | Tenant isolation |
DealId | route | Guid | Да | != Guid.Empty; существующая сделка (шаг 2) | — |
DocumentId | route | Guid | Да | != Guid.Empty; документ существует и DealId совпадает (шаг 2) | — |
Decision | body | DocumentReviewDecision | Да | enum строкой Accept/Reject; unknown → ValidationError | — |
RejectReason | body | string? | Условно | обязателен и 1..1000 при Decision = Reject; при Accept — пусто (INV-DOC-2) | свободный текст: в логи не целиком |
Decision — строгий enum DocumentReviewDecision { Accept, Reject } (не bool/строка), маппится в переход
Review(...). ActorUserId — из JWT (не из тела), пишется в ReviewedByUserId.
Модель ответа DealDocumentModel
Проекция агрегата DealDocumentEntity («один дом» — сущность; здесь только
возвращаемая дельта):
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | DealDocumentEntity.Id | — | Идентификатор документа. |
DealId | Guid | Да | DealDocumentEntity.DealId | — | Сделка (deal.md). |
Type | DealDocumentType | Да | DealDocumentEntity.Type (enum строкой) | — | Тип документа (напр. ContractDraftDocument). |
Status | DealDocumentStatus | Да | DealDocumentEntity.Status (enum строкой) | — | Accepted / Rejected после решения. |
Title | string | Да | DealDocumentEntity.Title | — | Отображаемое название. |
RejectReason | string? | Условно | DealDocumentEntity.RejectReason | — | Причина при Rejected; null при Accepted (US-INV-14). |
ReviewedByUserId | Guid | Да | DealDocumentEntity.ReviewedByUserId | — | Кто принял решение (= актор). |
ReviewedAt | DateTimeOffset | Да | DealDocumentEntity.ReviewedAt | — | Момент решения. |
Version | long | Да | DealDocumentEntity.Version | — | Для оптимистичной блокировки клиента. |
FileId/FileName/ContentType/SizeBytes в ответ не входят (скачивание — отдельным методом по
пресайн-ссылке storage). Все enum — строкой; дом значений — deal-document.md.
Алгоритм
- Контекст и доступ. Мутирует менеджер застройщика (
User, permissioncrm.deal-documents.review, иначеCRM_DOCUMENTS_FORBIDDEN); tenant —TenantId = DeveloperCompanyId. Идемпотентность не требуется — повтор на терминальном статусе →CRM_DOCUMENT_ALREADY_REVIEWED(шаг 2). - Проверка входа и предусловий.
DealId/DocumentId != Guid.Empty;Decision— известное значение enum (Accept/Reject); приDecision = Reject—RejectReasonне пуст и1..1000, приAccept—RejectReasonпуст — иначеVALIDATION_FAILED/CRM_DOCUMENT_REASON_REQUIRED(INV-DOC-2);- сделка существует в tenant застройщика — иначе
CRM_DEAL_NOT_FOUND(чужой tenant не раскрывается); - документ существует и его
DealIdсовпадает — иначеCRM_DOCUMENT_NOT_FOUND; - состояние допускает проверку:
document.Status ∈ {Uploaded, UnderReview}— иначеRequested(файла нет) →CRM_DOCUMENT_INVALID_STATUS;Accepted/Rejected(терминал) →CRM_DOCUMENT_ALREADY_REVIEWED; - доступ: тип документа — reviewable застройщиком (получатель = менеджер застройщика); тип, где застройщик —
загрузчик (
BookingContract/SaleContract/RosreestrDocument) →CRM_DOCUMENT_REVIEWER_FORBIDDEN(INV-DOC-2).
- Что читаем. Документ (мутируемый агрегат) под блокировкой строки — от гонки двойного решения; сделку-родителя — read, для guard tenant/статуса.
- Бизнес-правила и переходы. Решение проверки (INV-DOC-2: guard
Status ∈ {Uploaded, UnderReview}и «актор — сторона-проверяющий»): переход→ Accepted(приAccept:RejectReason = null) либо→ Rejected(приReject:RejectReasonзадан, иначеCRM_DOCUMENT_REASON_REQUIRED). Маппинг поле-в-поле:Status = decision,ReviewedByUserId = ActorUserId,ReviewedAt = now,RejectReasonпо решению,UpdatedByUserId = ActorUserId,Version += 1. Фиксируется доменный фактDocumentReviewed; подписчик материализует таймлайнDocumentAcceptedилиDocumentRejected(deal-timeline-event.md, INV-TLE-2). - Что меняется. Документ переходит в терминальный статус (
Accepted/Rejected), в ленту сделки добавляется строкаDocumentAccepted/DocumentRejected. Конкурентный доступ — по версии строки: рассинхрон при записи →CRM_CONCURRENCY_CONFLICT(перечитать). - События. Интеграционное
DocumentStatusChangedEvent→ топикpin.crm.document-status-changed.v1(ContractVersion = 1); payload{DocumentId, DealId, TenantId, DocumentType = Type, FromStatus (прежний), ToStatus = decision, RejectReason?, ChangedByUserId = ActorUserId, ChangedAt = ReviewedAt}(дом — document-status-changed.md). Потребительnotifications— статус документа инвестору (US-INV-14.3, US-INV-11). - Согласованность. Обновление документа, строка таймлайна и событие фиксируются в одном согласованном изменении (атомарно, INV-DOC-6).
- Результат.
DealDocumentModel(Id,DealId,Type,Status,Title,RejectReason,ReviewedByUserId,ReviewedAt,Version) → HTTP200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | DocumentStatusChangedEvent → pin.crm.document-status-changed.v1 (FromStatus → Accepted/Rejected), outbox |
| External calls | none (данные — своя схема crm; бинарь файла — storage, здесь не читается) |
| Cache / projections / search | таймлайн-строка DocumentAccepted/DocumentRejected (проекция карточки сделки) |
| Notifications | опосредованно: notifications — консюмер document-status-changed (статус документа инвестору, US-INV-14.3) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | неизвестный Decision, Guid.Empty в route | «Проверьте параметры запроса» | no retry |
CRM_DOCUMENT_REASON_REQUIRED | 400 | Decision = Reject без RejectReason (INV-DOC-2) | «Укажите причину отклонения» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
CRM_DOCUMENTS_FORBIDDEN | 403 | нет permission crm.deal-documents.review | «Недостаточно прав» | no retry |
CRM_DOCUMENT_REVIEWER_FORBIDDEN | 403 | актор не сторона-проверяющий типа (INV-DOC-2) | «Недостаточно прав для проверки документа» | no retry |
CRM_DEAL_NOT_FOUND | 404 | сделка не найдена / чужой tenant | «Сделка не найдена» | no retry |
CRM_DOCUMENT_NOT_FOUND | 404 | документ не найден или DealId не совпадает | «Документ не найден» | no retry |
CRM_DOCUMENT_INVALID_STATUS | 409 | статус не Uploaded/UnderReview (напр. Requested — файла нет) | «Документ не готов к проверке» | no retry |
CRM_DOCUMENT_ALREADY_REVIEWED | 409 | документ уже Accepted/Rejected (терминал) | «Решение по документу уже принято» | no retry |
CRM_CONCURRENCY_CONFLICT | 409 | рассинхрон RowVersion (xmin) при Update | «Документ изменён, обновите» | retry (перечитка) |
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор — менеджер застройщика с permission
crm.deal-documents.review; - сделка существует в
TenantId = DeveloperCompanyIdактора; документ существует, егоDealIdсовпадает; - тип документа — reviewable застройщиком (получатель = менеджер застройщика; типы, где застройщик загрузчик, —
BookingContract/SaleContract/RosreestrDocument— отклоняются, INV-DOC-2); - документ в статусе
Uploaded/UnderReview(неRequestedбез файла, не терминальный); - при
Decision = Rejectзадан непустойRejectReason(1..1000).
Постусловия (гарантированы после успеха):
- документ переведён в терминальный статус
Accepted(приAccept,RejectReason = null) либоRejected(приReject,RejectReasonзадан) сReviewedByUserId/ReviewedAt,Version+1; - в ленту сделки добавлена строка
DocumentAccepted/DocumentRejected(INV-TLE-2); в outbox —document-status-changed.v1; - при
Rejectedинвестор перезагружает документ новой записью (INV-DOC-4) — прежняя остаётся терминальной.
Интеграционная последовательность (после коммита)
DocumentStatusChangedEvent → pin.crm.document-status-changed.v1 (FromStatus → Accepted/Rejected,
RejectReason?) обрабатывается асинхронно; notifications доводит статус документа инвестору (US-INV-14.3,
US-INV-11). Инвестор видит решение с задержкой (окно eventual-consistency — секунды). Что предоставляет:
событие — additive-only, ключ-эффект идемпотентен по DocumentId; при Rejected payload несёт причину для
отображения инвестору.
Acceptance Criteria
- Принятие. Дано: документ
Uploaded/UnderReviewreviewable-типа. Когда:Decision = Accept. Тогда:200,Status = Accepted,RejectReason = null, строка лентыDocumentAccepted, событиеAccepted. - Отклонение с причиной. Дано: тот же документ. Когда:
Decision = Reject+RejectReason. Тогда:200,Status = Rejected, причина сохранена, строка лентыDocumentRejected, событиеRejected. - Отклонение без причины. Дано:
Decision = Reject,RejectReasonпуст. Когда: решение. Тогда:400 CRM_DOCUMENT_REASON_REQUIRED(INV-DOC-2). - Уже проверен / повтор. Дано: документ уже
Accepted/Rejected. Когда: повтор. Тогда:409 CRM_DOCUMENT_ALREADY_REVIEWED. - Файла нет. Дано: документ в статусе
Requested(файл не загружен). Когда: решение. Тогда:409 CRM_DOCUMENT_INVALID_STATUS. - Чужой проверяющий. Дано: тип, где застройщик — загрузчик (
RosreestrDocumentи т.п.). Когда: решение. Тогда:403 CRM_DOCUMENT_REVIEWER_FORBIDDEN(INV-DOC-2). - Двойное решение (гонка). Дано: два параллельных решения по документу. Когда: конфликт версии строки. Тогда:
409 CRM_CONCURRENCY_CONFLICT; проигравший перечитывает и видит терминальный статус.
Совместимость и наблюдаемость
- Новые optional-поля ответа — additive (без breaking); удаление/переименование/смена типа поля или значения
enum
DocumentReviewDecision/DealDocumentStatus— breaking (migration + rollout). UnknownDecisionво входе —ValidationError(не «тихий» no-op). - Logs:
deal document reviewed(DocumentId,DealId,TenantId,ToStatus;RejectReason— маскойreason=***); metrics:crm_deal_document_review_total{decision,result}, latency p95 (< 500 мс, SLO команд); traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь).
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: deal-document.md (дом полей/статусов,
Review, INV-DOC-2/4/6), deal-timeline-event.md (DocumentAccepted/DocumentRejected), deal.md. - Событие: document-status-changed.md; первичный запрос документа — document-requested.md; соседний метод области — list-deals.md.
- Контекст: README; tenancy — ADR-0052.