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 area | developer |
| Feature group | deal-documents |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/request-contract-documents.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика) |
| Auth policy / Permissions | crm.deal-documents.manage (JWT identity) |
| Scope (RBAC) | организация (застройщик) — TenantId = DeveloperCompanyId |
| Record-level | сделка своего TenantId (участник со стороны застройщика SellManager); Менеджер ПИН — platform-scope |
| Tenant isolation | TenantId в каждом запросе (= DeveloperCompanyId); разграничение применяется автоматически |
| Audit | create (DealDocumentEntity × N): RequestComment/Title — свободный текст менеджера, в логи не пишется |
Запрашивает документы менеджер застройщика (permission crm.deal-documents.manage) либо менеджер ПИН
(platform-scope) — только в рамках сделки своего TenantId = DeveloperCompanyId; чужой tenant не
раскрывается (CRM_DEAL_NOT_FOUND). Без нужного permission — CRM_DEAL_DOCUMENTS_FORBIDDEN.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/deals/{dealId}/documents/request |
| Success status | 201 (запросы созданы) |
| Idempotency header | не применяется (повтор создаёт новые Requested-документы; дедуп — на стороне UI) |
| Correlation | сквозной trace через заголовок traceparent |
Idempotency-заголовок не применяется: повторный вызов с тем же списком Documents создаёт новые
Requested-записи (существующие не переиспользуются) — дедуп остаётся на стороне UI.
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required (DeveloperCompanyId) | Tenant isolation |
ActorUserId | identity claims | Guid | Да | JWT (→ RequestedByUserId) | — |
DealId | route | Guid | Да | != Guid.Empty; существующая сделка своего TenantId (шаг 2) | — |
Documents | body | IReadOnlyList<RequestedDocumentItem> | Да | не пусто, 1..50 позиций | — |
DueAt | body | DateTimeOffset? | Нет | если задан — > now; применяется ко всем позициям батча | — |
Коллекция Documents живёт внутри именованной request-модели команды; тип позиции — именованная модель:
RequestedDocumentItem
| Поле | Тип | Обязательность | Валидация / Ограничения | Описание |
|---|---|---|---|---|
Title | string | Да | 1..256, не пустое после trim | Название запрашиваемого документа («Паспорт, стр. 1»). |
RequestComment | string? | Нет | 0..1000 | Что именно нужно — текст карточки «загрузите документы» (US-INV-16). |
Type во вход не входит — фиксируется системой = ContractDraftDocument. TargetUserId
не задаётся клиентом: резолвится как активный участник Investor сделки (шаг 3).
Модель ответа DealDocumentModel[]
Результат — массив только что созданных запросов (bounded, размер = число позиций Documents, 1..50),
не постраничный список: paging применяется к list/search-запросам (list-deals.md), здесь не
нужен. Поля — проекция дельты DealDocumentEntity:
DealDocumentModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | DealDocumentEntity.Id | — | Идентификатор запроса документа. |
DealId | Guid | Да | DealDocumentEntity.DealId | — | Сделка (deal.md). |
Type | DealDocumentType | Да | DealDocumentEntity.Type (enum строкой) | — | ContractDraftDocument. |
Status | DealDocumentStatus | Да | DealDocumentEntity.Status (enum строкой) | — | Requested (файла ещё нет). |
Title | string | Да | DealDocumentEntity.Title | — | Название документа. |
RequestComment | string? | Нет | DealDocumentEntity.RequestComment | — | Комментарий менеджера. |
DueAt | DateTimeOffset? | Нет | DealDocumentEntity.DueAt | — | Срок предоставления (индикация «просрочено»). |
TargetUserId | Guid? | Нет | активный DealMemberEntity.UserId (Role = Investor) | — | Кому адресован запрос; null — общий запрос без адресата. |
RequestedByUserId | Guid? | Нет | DealDocumentEntity.RequestedByUserId | — | Менеджер-инициатор (= ActorUserId). |
CreatedAt | DateTimeOffset | Да | 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событийDocumentRequestedEvent→pin.crm.document-requested.v1(через outbox); сама сделка не изменена.
Постусловия при отказе: ни одной записи документа не создано, строки таймлайна и события не публиковались.
Алгоритм
- Контекст и доступ. Выполняет менеджер застройщика (
User, permissioncrm.deal-documents.manage, иначеCRM_DEAL_DOCUMENTS_FORBIDDEN) либо менеджер ПИН (platform-scope); tenant —TenantId = DeveloperCompanyId. Идемпотентность не применяется — повтор создаёт новыеRequested-документы (дедуп на стороне UI). - Проверка входа и предусловий.
DealId != Guid.Empty;Documentsне пусто иCount ∈ 1..50; каждыйTitle—1..256не пустое после trim;RequestComment—0..1000;DueAt(если задан) —> now— иначеVALIDATION_FAILED;- сделка существует в tenant застройщика — иначе
CRM_DEAL_NOT_FOUND(чужой tenant не раскрывается); - допустимость состояния (INV-DOC-5):
Status ∉ {Completed, Cancelled}— иначеCRM_DEAL_ALREADY_TERMINAL; scope — застройщик своегоTenantId.
- Что читаем. Сделку (проверки выше) и активного адресата: участник со
Role = Investor, ещё не удалённый из сделки (INV-DEAL-3) →TargetUserId. Если активного инвестора нет — общий запрос без адресата (карточка/событие видны всем участникам). - Бизнес-правила и переходы. Для каждой позиции
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 }). Маппинг поле-в-поле — таблицы выше. - Что меняется. Появляются N записей документов в статусе
Requested; в ленту добавляются строкиDocumentRequested. Сама сделка не меняется (документ — отдельная таблица). - События. По одному
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). - Согласованность. Все документы, строки таймлайна и события фиксируются в одном согласованном изменении (атомарно).
- Результат. Массив созданных запросов
DealDocumentModel[]→ HTTP201.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | DocumentRequestedEvent → pin.crm.document-requested.v1 (по одному на созданный документ), outbox |
| External calls | none (данные — своя схема crm; ФИО/телефон адресата резолвит consumer из реплики identity) |
| Cache / projections / search | none (прямая вставка; таймлайн — локальная материализация доменных событий) |
| Notifications | опосредованно: messenger — карточка «загрузите документы» (US-INV-16); notifications — пуш/СМС адресату (консюмеры document-requested) |
Интеграционная последовательность и окно согласованности
- Синхронно (в одном изменении): созданы
Nзапросов документов,Nстрок таймлайна,Nсобытий положены в outbox — ответ201менеджеру. На вкладке «Документы» карточки сделки застройщик сразу видит позиции в статусеRequested. - Асинхронно: каждое
document-requested.v1доставляется потребителям:messengerпоказывает адресату карточку «загрузите документы» (US-INV-16);notificationsшлёт адресату пуш/СМС (US-INV-11), резолвя ФИО/телефон из репликиidentity.
- Окно eventual-consistency = задержка outbox → консюмер: до её завершения инвестор ещё не видит карточку и
уведомление, тогда как застройщик уже наблюдает запросы. Порядок по документу сохраняется ключом
партиционирования
AggregateId = DocumentId; повторная доставка обрабатывается идемпотентно.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | пустой Documents, Count > 50, пустой/длинный Title, RequestComment > 1000, DueAt <= now, Guid.Empty | «Проверьте список запрашиваемых документов» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
CRM_DEAL_DOCUMENTS_FORBIDDEN | 403 | нет permission crm.deal-documents.manage | «Недостаточно прав» | no retry |
CRM_DEAL_NOT_FOUND | 404 | сделка не найдена / чужой TenantId | «Сделка не найдена» | no retry |
CRM_DEAL_ALREADY_TERMINAL | 409 | INV-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; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь).
Acceptance criteria
- Given сделка не терминальна и список из
1..50позиций, When менеджер запрашивает документы, Then201с массивомDealDocumentModel[](всеRequested,Type = ContractDraftDocument), опубликованоNсобытийdocument-requested.v1. - Given у сделки есть активный участник
Investor, When запрос, ThenTargetUserId= егоUserId; если активного инвестора нет —TargetUserId = null(общий запрос, виден всем участникам). - Given тот же список отправлен повторно, When запрос, Then создаются новые
Requested-документы (операция неидемпотентна; дедуп — на стороне UI). - Given
Documentsпуст или> 50позиций, пустой/длинныйTitle,RequestComment > 1000илиDueAt <= now, When запрос, Then400 VALIDATION_FAILED. - Given сделка
Completed/Cancelled, When запрос, Then409 CRM_DEAL_ALREADY_TERMINAL(INV-DOC-5). - Given сделка чужого
TenantId/несуществующая, When запрос, Then404 CRM_DEAL_NOT_FOUND; безcrm.deal-documents.manage—403 CRM_DEAL_DOCUMENTS_FORBIDDEN.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): POST /api/investor/crm/deals/{dealId}/documents — Загрузить документ в сделку
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: deal-document.md (дом полей,
DealDocumentType/DealDocumentStatus, INV-DOC-5,DealDocument.Request), deal.md (статус-guard, активныйInvestorINV-DEAL-3), deal-timeline-event.md (DocumentRequestedInProgress+DueAt, INV-TLE-3,TimelineDocumentPayload). - Событие: document-requested.md; соседнее — статус документа document-status-changed.md.
- Контекст: README; tenancy — ADR-0052.