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

POST /api/developer/crm/bookings/{bookingId}/confirm-documents — Подтвердить документы брони

Менеджер застройщика подтверждает получение документов для бронирования — фактически это приём (Accept) над паспортными документами брони (тип ClientPassport), загруженными инвестором/агентом. Проверяющий — сторона-получатель (застройщик, INV-DOC-2), поэтому чужой актор получает CRM_DOCUMENT_REVIEWER_FORBIDDEN. Документы переходят Uploaded/UnderReview → Accepted с проставлением ReviewedByUserId/ReviewedAt, лента сделки получает событие DocumentAccepted. Дом полей — deal-document.md.

Назначение

По кнопке «Подтвердить документы» карточки брони менеджер застройщика переводит паспортные документы брони в Accepted (US-INV-14 — инвестор видит «принято»). Вызывает менеджер застройщика; тела ответа нет. По каждому подтверждённому документу публикуется интеграционное document-status-changed для уведомления инвестора.

Идемпотентность

Отдельного ключа идемпотентности нет — естественная: если у брони нет паспортных документов в Uploaded/ UnderReview (все уже терминальны), повторный вызов возвращает 204 без изменений, строк ленты и событий.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areadeveloper (JWT менеджера застройщика)
Feature groupdeal-documents
Статусdraft
Документdocs/06-services/crm/api/developer/confirm-booking-documents.md

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

ПроверкаЗначение
Actor typeUser (Менеджер застройщика); Operator (Менеджер ПИН — platform-scope, арбитраж)
Auth policy / Permissionscrm.deal-documents.review (сторона-получатель паспортных документов)
Scope (RBAC)организация (застройщик): tenant + record-level по участию в сделке
Record-levelактор — SellManager/менеджер застройщика сделки брони (booking.DealId) либо Менеджер ПИН
Tenant isolationTenantId = DeveloperCompanyId из claims; бронь и документы читаются только в этом tenant (booking.TenantId == actor.TenantId, ADR-0052)
Auditupdate-review (ReviewedByUserId = actor.UserId); имена/номера паспортов в логи не пишутся (только DocumentId)
Доступ

INV-DOC-2: проверяющий — сторона-получатель документа. Для ClientPassport это менеджер застройщика сделки брони (SellManager, record-level по booking.DealId) либо Менеджер ПИН (platform-scope, арбитраж). Чужой актор (в т.ч. агентская сторона, загрузившая документ) получает CRM_DOCUMENT_REVIEWER_FORBIDDEN.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developer/crm/bookings/{bookingId}/confirm-documents
Success status204 (подтверждено; повтор без ожидающих документов — тот же 204, идемпотентно)
Idempotency headerне применяется (естественная идемпотентность: нет документов в Uploaded/UnderReview → no-op)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired; = DeveloperCompanyId актораTenant isolation
BookingIdrouteGuidДа!= Guid.Empty; существующая бронь в tenant (шаг 2)
DocumentIdsbodyIReadOnlyList<Guid>?Нетесли задан — 1..50 элементов, каждый != Guid.Empty, все принадлежат брони и типу ClientPassport (шаг 3); null → подтверждаются все ожидающие проверки паспортные документы брони
ActorUserIdidentity claimsGuidДаJWT менеджера застройщика; пишется в ReviewedByUserId

DocumentIds — единственная коллекция тела, живёт внутри named-модели ConfirmBookingDocumentsCommand (container). Вложенных сущностей нет: дом полей документа — deal-document.md.

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

Тела нет — по успеху HTTP 204. Актуальные статусы документов и запись таймлайна DocumentAccepted читаются отдельными query карточки сделки/брони (обновляются тем же изменением).

Алгоритм

  1. Контекст и доступ. Выполняет менеджер застройщика (User, permission crm.deal-documents.review) либо менеджер ПИН (platform-scope, арбитраж); tenant — TenantId = DeveloperCompanyId. Естественная идемпотентность: если нет документов в Uploaded/UnderReview — no-op (шаг 3), отдельного ключа нет.
  2. Проверка входа и предусловий.
    • BookingId != Guid.Empty; при заданном DocumentIds1..50 элементов, каждый != Guid.Empty, без дублей — иначе VALIDATION_FAILED;
    • бронь существует в tenant застройщика — иначе CRM_BOOKING_NOT_FOUND (чужой tenant не раскрывается: 404);
    • состояние допускает подтверждение: booking.Status ∈ {Reserved, AwaitingPayment, Booked} (нетерминальная бронь, INV-DOC-5) — иначе CRM_BOOKING_ALREADY_TERMINAL (на отменённой/истёкшей броне подтверждать нечего);
    • доступ: актор — SellManager/менеджер застройщика сделки брони (record-level по booking.DealId) либо менеджер ПИН — иначе CRM_DOCUMENT_REVIEWER_FORBIDDEN.
  3. Что читаем. Паспортные документы брони (тип ClientPassport, не удалённые; при заданном DocumentIds — только их) под блокировкой строк (гонка с повторной загрузкой инвестором):
    • если выборка пуста: при заданном DocumentIds часть id не найдена/не паспорт/не этой брони → CRM_DOCUMENT_NOT_FOUND; иначе паспортных документов у брони нет → CRM_DOCUMENTS_NOT_UPLOADED;
    • ожидающие проверки — документы в Uploaded/UnderReview; если таких нет (все уже Accepted/Rejected — терминальны, перезалив идёт новой записью, INV-DOC-4) → идемпотентный no-op: 204, шаги 4–7 не выполняются.
  4. Бизнес-правила и переходы. Каждый ожидающий документ принимается (INV-DOC-2: проверяющий = сторона-получатель типа ClientPassport, т.е. менеджер застройщика; иначе CRM_DOCUMENT_REVIEWER_FORBIDDEN): переход Uploaded/UnderReview → Accepted; маппинг: Status = Accepted, ReviewedByUserId = ActorUserId, ReviewedAt = now, UpdatedByUserId = ActorUserId, UpdatedAt = now. Фиксируется доменный факт DocumentReviewed; в той же транзакции материализуется запись ленты (INV-TLE-2): DealTimelineEventEntity(Type = DocumentAccepted, Status = Done, OccurredAt = now, ActorRole = SellManager, Payload = TimelineDocumentPayload { DocumentId, DocumentType = "ClientPassport", Title, RequestComment }) — дом типов/payload deal-timeline-event.md.
  5. Что меняется. Ожидающие паспортные документы переходят в Accepted; в ленту добавляются append-only строки DocumentAccepted (INV-TLE-1). Бронь не мутируется. Конкурентный доступ — по версии строки: если инвестор перезалил документ параллельно → ретрай с перечиткой шага 3.
  6. События. По каждому подтверждённому документу — интеграционное DocumentStatusChangedEvent → топик pin.crm.document-status-changed.v1 (ContractVersion = 1); payload: DocumentType = ClientPassport, FromStatus (Uploaded/UnderReview), ToStatus = Accepted, RejectReason = null, ChangedByUserId = ReviewedByUserId, ChangedAt = ReviewedAt (дом — document-status-changed.md, INV-DOC-6). Потребитель notifications — инвестору «документ принят».
  7. Согласованность. Обновление всех документов, строки таймлайна и события фиксируются в одном согласованном изменении (атомарно).
  8. Результат. Тела ответа нет → HTTP 204 No Content.

Диаграмма

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

ТипДетали
Integration eventsDocumentStatusChangedEventpin.crm.document-status-changed.v1 (по одному на подтверждённый документ, ToStatus = Accepted), outbox
External callsnone (бинарь файла не читается — подтверждаются метаданные)
Cache / projections / searchобновляется ix_deal_documents_status; лента таймлайна получает строки DocumentAccepted
Notificationsопосредованно: notifications — консюмер document-status-changed (инвестору «документ принят», US-INV-14.3) и TimelineItemAdded (US-INV-11)

Ошибки и failure modes

Ошибки

Два кода различают отсутствие документов от неверной адресации: CRM_DOCUMENTS_NOT_UPLOADED — у брони вовсе нет паспортных документов; CRM_DOCUMENT_NOT_FOUND — заданный DocumentIds не паспорт или не этой брони. Ни тот, ни другой не путать с идемпотентным no-op (шаг 3): если все паспортные документы уже терминальны (Accepted/Rejected), ответ — 204 без ошибки.

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400формат BookingId/DocumentIds«Проверьте корректность запроса»no retry
CRM_BOOKING_NOT_FOUND404бронь не найдена в tenant застройщика«Бронирование не найдено»no retry
CRM_BOOKING_ALREADY_TERMINAL409booking.Status терминальный (Completed/*Expired/Cancelled)«Бронирование завершено — подтверждение недоступно»no retry
CRM_DOCUMENTS_NOT_UPLOADED409у брони нет паспортных документов для подтверждения«Документы ещё не загружены»no retry (ждать загрузку)
CRM_DOCUMENT_NOT_FOUND404заданный DocumentIds не принадлежит брони / не тип ClientPassport«Документ не найден»no retry
CRM_DOCUMENT_REVIEWER_FORBIDDEN403INV-DOC-2: актор не сторона-получатель (не менеджер застройщика сделки)«Нет прав на подтверждение документов»no retry

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

Предусловия (истинны до сценария):

  • актор — менеджер застройщика сделки брони (SellManager) либо Менеджер ПИН с permission crm.deal-documents.review (сторона-получатель паспортных документов, INV-DOC-2);
  • бронь существует в TenantId = DeveloperCompanyId актора и нетерминальна (Reserved/AwaitingPayment/Booked, INV-DOC-5);
  • при заданном DocumentIds все id — паспортные документы (ClientPassport) этой брони.

Постусловия (гарантированы после успеха):

  • каждый ожидающий паспортный документ (Uploaded/UnderReview) переведён в Accepted с ReviewedByUserId/ ReviewedAt; уже терминальные (Accepted/Rejected) не трогаются;
  • по каждому подтверждённому документу записана строка ленты DocumentAccepted (INV-TLE-1) и в outbox — событие document-status-changed.v1 (ToStatus = Accepted); бронь не мутируется;
  • если ожидающих документов не было — идемпотентный no-op: 204, строк ленты и событий нет.

Интеграционная последовательность (после коммита)

По каждому подтверждённому документу DocumentStatusChangedEvent → pin.crm.document-status-changed.v1 обрабатывается асинхронно; потребитель notifications уведомляет инвестора «документ принят» (US-INV-14.3). Инвестор видит «принято» с задержкой (окно eventual-consistency — секунды); в промежутке карточка ещё показывает прежний статус. Что предоставляет: событие — additive-only, по одному на документ, ключ-эффект идемпотентен по DocumentId.

Acceptance Criteria

  • Happy path (все). Дано: у брони есть паспортные документы в Uploaded/UnderReview, DocumentIds не задан. Когда: подтверждение. Тогда: 204, все ожидающие → Accepted, по строке ленты и событию на каждый документ.
  • Подмножество. Дано: задан DocumentIds из части документов брони. Когда: подтверждение. Тогда: подтверждены только указанные.
  • Идемпотентный повтор. Дано: все паспортные документы уже Accepted. Когда: повтор. Тогда: 204 no-op, строк ленты и событий нет.
  • Документы не загружены. Дано: у брони нет паспортных документов. Когда: подтверждение. Тогда: 409 CRM_DOCUMENTS_NOT_UPLOADED.
  • Некорректные DocumentIds. Дано: id не паспорт / не этой брони. Когда: подтверждение. Тогда: 404 CRM_DOCUMENT_NOT_FOUND.
  • Терминальная бронь. Дано: бронь Cancelled/*Expired/Completed. Когда: подтверждение. Тогда: 409 CRM_BOOKING_ALREADY_TERMINAL.
  • Чужой проверяющий. Дано: актор не сторона-получатель (не менеджер застройщика сделки). Когда: подтверждение. Тогда: 403 CRM_DOCUMENT_REVIEWER_FORBIDDEN (INV-DOC-2).
  • Гонка перезалива. Дано: инвестор параллельно перезалил документ. Когда: конфликт версии строки. Тогда: ретрай с перечиткой ожидающих (шаг 3).

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

  • DocumentIds — additive optional (при отсутствии — подтверждаются все ожидающие); удаление/переименование поля или смена Success status — breaking (migration + rollout). Контракт события — additive-only (document-status-changed.md).
  • Logs: booking-documents confirmed (BookingId, DealId, count, DocumentId[]; ФИО/паспорт — не пишутся); metrics: crm_document_reviews_total{decision="accept"}, latency p95; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь).

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

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

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