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

POST /api/developer/crm/deals/{dealId}/documents/request — Запросить документы ДДУ/ДКП

Менеджер застройщика запрашивает у клиента пакет документов для формирования договора ДДУ/ДКП (US-DEV-05). Каждая запрошенная позиция создаёт DealDocumentEntity в статусе Requested с Type = ContractDraftDocument, RequestComment, DueAt и TargetUserId = активный инвестор сделки; пишется таймлайн-событие DocumentRequested (US-INV-09) и публикуется DocumentRequestedEvent — карточка «загрузите документы» в чате (US-INV-16) и уведомление адресату.

Назначение

Точка входа вкладки «Документы» карточки сделки ЛК застройщика: менеджер формирует список нужных для ДДУ/ДКП документов (например «Паспорт, стр. 1», «Свидетельство о браке», «СНИЛС») с комментарием и сроком, а CRM заводит соответствующие запросы и адресует их инвестору сделки.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areadeveloper
Feature groupdeal-documents
Статусdraft
Документdocs/06-services/crm/api/developer/request-contract-documents.md

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

ПроверкаЗначение
Actor typeUser (Менеджер застройщика)
Auth policy / Permissionscrm.deal-documents.manage (JWT identity)
Scope (RBAC)организация (застройщик) — TenantId = DeveloperCompanyId
Record-levelсделка своего TenantId (участник со стороны застройщика SellManager); Менеджер ПИН — platform-scope
Tenant isolationTenantId в каждом запросе (= DeveloperCompanyId); разграничение применяется автоматически
Auditcreate (DealDocumentEntity × N): RequestComment/Title — свободный текст менеджера, в логи не пишется
Доступ

Запрашивает документы менеджер застройщика (permission crm.deal-documents.manage) либо менеджер ПИН (platform-scope) — только в рамках сделки своего TenantId = DeveloperCompanyId; чужой tenant не раскрывается (CRM_DEAL_NOT_FOUND). Без нужного permission — CRM_DEAL_DOCUMENTS_FORBIDDEN.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developer/crm/deals/{dealId}/documents/request
Success status201 (запросы созданы)
Idempotency headerне применяется (повтор создаёт новые Requested-документы; дедуп — на стороне UI)
Correlationсквозной trace через заголовок traceparent
Неидемпотентность

Idempotency-заголовок не применяется: повторный вызов с тем же списком Documents создаёт новые Requested-записи (существующие не переиспользуются) — дедуп остаётся на стороне UI.

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired (DeveloperCompanyId)Tenant isolation
ActorUserIdidentity claimsGuidДаJWT (→ RequestedByUserId)
DealIdrouteGuidДа!= Guid.Empty; существующая сделка своего TenantId (шаг 2)
DocumentsbodyIReadOnlyList<RequestedDocumentItem>Дане пусто, 1..50 позиций
DueAtbodyDateTimeOffset?Нетесли задан — > now; применяется ко всем позициям батча

Коллекция Documents живёт внутри именованной request-модели команды; тип позиции — именованная модель:

RequestedDocumentItem

ПолеТипОбязательностьВалидация / ОграниченияОписание
TitlestringДа1..256, не пустое после trimНазвание запрашиваемого документа («Паспорт, стр. 1»).
RequestCommentstring?Нет0..1000Что именно нужно — текст карточки «загрузите документы» (US-INV-16).

Type во вход не входит — фиксируется системой = ContractDraftDocument. TargetUserId не задаётся клиентом: резолвится как активный участник Investor сделки (шаг 3).

Модель ответа DealDocumentModel[]

Результат — массив только что созданных запросов (bounded, размер = число позиций Documents, 1..50), не постраничный список: paging применяется к list/search-запросам (list-deals.md), здесь не нужен. Поля — проекция дельты DealDocumentEntity:

DealDocumentModel

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаDealDocumentEntity.IdИдентификатор запроса документа.
DealIdGuidДаDealDocumentEntity.DealIdСделка (deal.md).
TypeDealDocumentTypeДаDealDocumentEntity.Type (enum строкой)ContractDraftDocument.
StatusDealDocumentStatusДаDealDocumentEntity.Status (enum строкой)Requested (файла ещё нет).
TitlestringДаDealDocumentEntity.TitleНазвание документа.
RequestCommentstring?НетDealDocumentEntity.RequestCommentКомментарий менеджера.
DueAtDateTimeOffset?НетDealDocumentEntity.DueAtСрок предоставления (индикация «просрочено»).
TargetUserIdGuid?Нетактивный DealMemberEntity.UserId (Role = Investor)Кому адресован запрос; null — общий запрос без адресата.
RequestedByUserIdGuid?НетDealDocumentEntity.RequestedByUserIdМенеджер-инициатор (= ActorUserId).
CreatedAtDateTimeOffsetДаDealDocumentEntity.CreatedAtМомент создания запроса.

Все enum — строкой; дом значений — deal-document.md.

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

Предусловия (должны быть истинны до вызова):

  • сделка существует в TenantId актора и не в терминальном статусе (Status ∉ {Completed, Cancelled}, INV-DOC-5);
  • список Documents непуст и содержит 1..50 позиций;
  • актор — менеджер застройщика с crm.deal-documents.manage (или менеджер ПИН, platform-scope).

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

  • создано N = Documents.Count записей DealDocumentEntity в статусе Requested (Type = ContractDraftDocument), TargetUserId = активный участник Investor сделки (или null, если активного инвестора нет);
  • в таймлайн добавлено N строк DocumentRequested (Status = InProgress, закрываются парным DocumentUploaded, INV-TLE-3);
  • опубликовано N событий DocumentRequestedEventpin.crm.document-requested.v1 (через outbox); сама сделка не изменена.

Постусловия при отказе: ни одной записи документа не создано, строки таймлайна и события не публиковались.

Алгоритм

  1. Контекст и доступ. Выполняет менеджер застройщика (User, permission crm.deal-documents.manage, иначе CRM_DEAL_DOCUMENTS_FORBIDDEN) либо менеджер ПИН (platform-scope); tenant — TenantId = DeveloperCompanyId. Идемпотентность не применяется — повтор создаёт новые Requested-документы (дедуп на стороне UI).
  2. Проверка входа и предусловий.
    • DealId != Guid.Empty; Documents не пусто и Count ∈ 1..50; каждый Title1..256 не пустое после trim; RequestComment0..1000; DueAt (если задан) — > now — иначе VALIDATION_FAILED;
    • сделка существует в tenant застройщика — иначе CRM_DEAL_NOT_FOUND (чужой tenant не раскрывается);
    • допустимость состояния (INV-DOC-5): Status ∉ {Completed, Cancelled} — иначе CRM_DEAL_ALREADY_TERMINAL; scope — застройщик своего TenantId.
  3. Что читаем. Сделку (проверки выше) и активного адресата: участник со Role = Investor, ещё не удалённый из сделки (INV-DEAL-3) → TargetUserId. Если активного инвестора нет — общий запрос без адресата (карточка/событие видны всем участникам).
  4. Бизнес-правила и переходы. Для каждой позиции Documents создаётся запрос документа: переход [*] → Requested, Type = ContractDraftDocument, Title, RequestComment, DueAt, RequestedByUserId = ActorUserId, TargetUserId, CreatedAt = now. Фиксируется доменный факт DocumentRequested; его подписчик материализует строку таймлайна (Type = DocumentRequested, Status = InProgress, DueAt; закрывается парным DocumentUploaded — INV-TLE-3; Payload = TimelineDocumentPayload { DocumentId, DocumentType = ContractDraftDocument, Title, RequestComment }). Маппинг поле-в-поле — таблицы выше.
  5. Что меняется. Появляются N записей документов в статусе Requested; в ленту добавляются строки DocumentRequested. Сама сделка не меняется (документ — отдельная таблица).
  6. События. По одному DocumentRequestedEvent на каждый созданный документ → топик pin.crm.document-requested.v1 (ContractVersion = 1, AggregateId = DocumentId, AggregateType = "DealDocument"); payload {DocumentId, DealId, TenantId, DocumentType = ContractDraftDocument, Title, RequestComment?, RequestedByUserId, TargetParticipantUserId = TargetUserId, RequestedAt = CreatedAt, DueAt?} — через outbox (дом — document-requested.md). Потребители: messenger (карточка «загрузите документы», US-INV-16), notifications (пуш/СМС адресату, US-INV-11).
  7. Согласованность. Все документы, строки таймлайна и события фиксируются в одном согласованном изменении (атомарно).
  8. Результат. Массив созданных запросов DealDocumentModel[] → HTTP 201.

Диаграмма

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

ТипДетали
Integration eventsDocumentRequestedEventpin.crm.document-requested.v1 (по одному на созданный документ), outbox
External callsnone (данные — своя схема crm; ФИО/телефон адресата резолвит consumer из реплики identity)
Cache / projections / searchnone (прямая вставка; таймлайн — локальная материализация доменных событий)
Notificationsопосредованно: messenger — карточка «загрузите документы» (US-INV-16); notifications — пуш/СМС адресату (консюмеры document-requested)

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

  1. Синхронно (в одном изменении): созданы N запросов документов, N строк таймлайна, N событий положены в outbox — ответ 201 менеджеру. На вкладке «Документы» карточки сделки застройщик сразу видит позиции в статусе Requested.
  2. Асинхронно: каждое document-requested.v1 доставляется потребителям:
    • messenger показывает адресату карточку «загрузите документы» (US-INV-16);
    • notifications шлёт адресату пуш/СМС (US-INV-11), резолвя ФИО/телефон из реплики identity.
  3. Окно eventual-consistency = задержка outbox → консюмер: до её завершения инвестор ещё не видит карточку и уведомление, тогда как застройщик уже наблюдает запросы. Порядок по документу сохраняется ключом партиционирования AggregateId = DocumentId; повторная доставка обрабатывается идемпотентно.

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400пустой Documents, Count > 50, пустой/длинный Title, RequestComment > 1000, DueAt <= now, Guid.Empty«Проверьте список запрашиваемых документов»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
CRM_DEAL_DOCUMENTS_FORBIDDEN403нет permission crm.deal-documents.manage«Недостаточно прав»no retry
CRM_DEAL_NOT_FOUND404сделка не найдена / чужой TenantId«Сделка не найдена»no retry
CRM_DEAL_ALREADY_TERMINAL409INV-DOC-5: deal.Status ∈ {Completed, Cancelled}«Сделка завершена — запрос документов недоступен»no retry

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

  • Новые optional поля запроса/ответа — additive (без breaking); удаление/переименование/смена типа поля или значения enum — breaking (migration + rollout). Unknown значение enum во входе — ValidationError.
  • Logs: contract documents requested (TenantId, DealId, Count, TargetUserId; Title/RequestComment — не в логи); metrics: crm_document_requests_total{result}, latency p95 (< 500 мс, SLO команд); traces — OTel, correlation через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь).

Acceptance criteria

  • Given сделка не терминальна и список из 1..50 позиций, When менеджер запрашивает документы, Then 201 с массивом DealDocumentModel[] (все Requested, Type = ContractDraftDocument), опубликовано N событий document-requested.v1.
  • Given у сделки есть активный участник Investor, When запрос, Then TargetUserId = его UserId; если активного инвестора нет — TargetUserId = null (общий запрос, виден всем участникам).
  • Given тот же список отправлен повторно, When запрос, Then создаются новые Requested-документы (операция неидемпотентна; дедуп — на стороне UI).
  • Given Documents пуст или > 50 позиций, пустой/длинный Title, RequestComment > 1000 или DueAt <= now, When запрос, Then 400 VALIDATION_FAILED.
  • Given сделка Completed/Cancelled, When запрос, Then 409 CRM_DEAL_ALREADY_TERMINAL (INV-DOC-5).
  • Given сделка чужого TenantId/несуществующая, When запрос, Then 404 CRM_DEAL_NOT_FOUND; без crm.deal-documents.manage403 CRM_DEAL_DOCUMENTS_FORBIDDEN.

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

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

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