UC-CRM-004. Документы сделки: запрос → загрузка → проверка → статусы
Назначение
Жизненный цикл документа сделки: менеджер застройщика запрашивает документ у клиента → инвестор (или агент за него) загружает файл → менеджер принимает/отклоняет → статусы и уведомления доходят до участников. Дом полей документа — ../domain/deal-document.md; здесь описаны только поток и дельта.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CRM-004 |
| Контекст | crm (см. docs/06-services/crm/) |
| Тип | основной |
| Статус | draft |
Актор
Сценарий двухсторонний: запрос/проверку выполняет застройщик, загрузку — клиентская сторона.
| Роль | Permissions / scope | Tenant | Record-level |
|---|---|---|---|
| Менеджер застройщика (запрос + проверка) | crm.deal-documents.manage / .review | TenantId = DeveloperCompanyId (ADR-0052) | видит только сделки своего TenantId |
| Инвестор, либо Агент за него (загрузка/скачивание) | crm.deal-documents.manage (upload) / .view (download) | Инвестор — TenantId = null; Агент — AgencyCompanyId | участник сделки (PrimaryInvestorUserId/ExecutorAgentUserId = actor) |
| Менеджер ПИН | crm.deal-documents.* (platform-scope) | кросс-tenant | любой (арбитраж, US-MGR-04) |
Предусловие
- Сделка существует и в нетерминальном статусе (
Status ∉ {Completed, Cancelled}, INV-DOC-5; ../domain/deal.md, enumDealStatus). - У сделки есть активный участник
InvestorPrimary(INV-DEAL-3) — адресат запроса и владелец клиентских типов. - Тип документа принадлежит клиентской стороне (
ContractDraftDocument,ClientPassport,SignedSaleContractи т.п. — таблицаDealDocumentTypeв ../domain/deal-document.md). - Файл предварительно загружен в storage/CDN, у клиента есть
FileId.
Триггер
| Поле | Значение |
|---|---|
| Источник | UI-действие → вызов API (три операции) + расписание (worker просрочки) |
| Триггерящий элемент | «Запросить документы» застройщика → POST /api/developer/crm/deals/{id}/documents/request; «Загрузить» инвестора → POST /api/investor/crm/deals/{id}/documents/{docId}/upload; «Принять/Отклонить» → POST /api/developer/crm/deals/{id}/documents/{docId}/confirm; worker timeline-overdue |
Основной Поток
Поток охватывает три фазы жизненного цикла документа.
Фаза 1. Запрос документа (менеджер застройщика)
- Менеджер на вкладке «Документы» карточки сделки запрашивает документы для формирования договора ДДУ/ДКП.
Модель запроса
RequestContractDocumentsRequest { Items: RequestDocumentItem[] }, гдеRequestDocumentItem { Type: DealDocumentType, Title, RequestComment?, DueAt? }. - Проверка входа.
Itemsне пусты;Type/Titleзаданы;Type— клиентский. - Проверки состояния. Сделка существует и нетерминальна (
Status ∉ {Completed, Cancelled}, INV-DOC-5, иначеCRM_DEAL_ALREADY_TERMINAL); tenant совпадает (deal.TenantId == actor.TenantId);targetInvestorId= активный участник с рольюInvestorPrimary. - Что меняется. Для каждого item создаётся
DealDocumentEntity:Status = Requested,RequestedByUserId = actor,FileId = null; событиеDocumentRequested. Пишется таймлайнDealTimelineEventEntity(DocumentRequested)(Status = InProgress,DueAtиз item — INV-TLE-3). Всё — в одном согласованном изменении вместе с записью в outbox. - Публикация. На каждый документ —
DocumentRequestedEvent(топикpin.crm.document-requested.v1,TargetParticipantUserId = targetInvestorId) через outbox. Результат — списокDocumentIds. - Асинхронно.
messengerрисует карточку целевого действия «загрузите документы» в чат сделки (US-INV-16),notificationsшлёт «требуется загрузка».
Фаза 2. Загрузка файла (инвестор / агент за него)
- Инвестор выбирает файл; после заливки в storage вызывает
POST /api/investor/crm/deals/{id}/documents/{docId}/uploadсUploadDealDocumentRequest { FileId, FileName, ContentType, SizeBytes, RequisiteSetId? }. - Проверка входа (INV-DOC-3).
ContentType ∈ {application/pdf, image/jpeg, image/png},SizeBytes ∈ (0; 52 428 800]— иначеCRM_DOCUMENT_FILE_INVALID. - Проверки состояния. Документ в
Status = Requested(или запись создаётся при прямой загрузке — A1); сделка нетерминальна (INV-DOC-5); INV-DOC-1 — actor является владельцемType(инвестор/агент клиентских типов), иначеCRM_DOCUMENT_UPLOADER_FORBIDDEN. - Переходы и что меняется.
Requested → Uploaded; для типов с проверкой — далее→ UnderReview(таблица владельцев ../domain/deal-document.md);UploadedByUserId/UploadedAtпроставлены. Пишется таймлайнDocumentUploaded(Status = Done, закрывает парныйDocumentRequested→Done/CompletedAt, INV-TLE-3). СобытиеDocumentUploaded. - Публикация.
DocumentStatusChangedEvent(топикpin.crm.document-status-changed.v1,FromStatus = Requested,ToStatus = Uploaded|UnderReview) через outbox. Результат —DocumentId,Status.
Фаза 3. Проверка и статусы (менеджер застройщика)
- Менеджер видит документ
UnderReviewи принимает решение:POST /api/developer/crm/deals/{id}/documents/{docId}/confirmсConfirmContractDocumentsRequest { Decision: DocumentReviewDecision (Accept|Reject), RejectReason? }. - Проверки состояния. Документ в
Status = UnderReview(иначеCRM_DOCUMENT_STATUS_INVALID); INV-DOC-2 — actor является стороной-проверяющим (SellManager/PinManagerTenantId), иначеCRM_DOCUMENT_REVIEWER_FORBIDDEN; приRejectRejectReasonобязателен, иначеCRM_DOCUMENT_REASON_REQUIRED. - Переходы и что меняется.
UnderReview → Accepted|Rejected(терминальные);ReviewedByUserId/ReviewedAt, при отклоненииRejectReason. Пишется таймлайнDocumentAccepted|DocumentRejected(Status = Done; приRejectedpayloadTimelineDocumentRejectedPayload.RejectReason). СобытиеDocumentReviewed. - Публикация.
DocumentStatusChangedEvent(ToStatus = Accepted|Rejected,RejectReason?) через outbox, атомарно с переходом.notificationsуведомляет инвестора о статусе (US-INV-14.3). - Скачивание (в любой момент).
GET /api/investor/crm/deals/{id}/documents/{docId}/downloadрезолвит короткоживущую пресайн-ссылку storage (tenantId + fileId) после проверки участия (US-INV-13); бинарь через CRM не проходит.
CRM валидирует только метаданные файла (ContentType, SizeBytes — INV-DOC-3), но не содержимое
бинаря; FileId принимается на доверии (файл заливается в storage/CDN до вызова). Отсутствующий/недозалитый
бинарь проявится при скачивании (пресайн-ссылка вернёт ошибку storage), а не при загрузке метаданных.
Конкурентная проверка: параллельные confirm двумя проверяющими или confirm во время повторной загрузки
разрешаются guard-ом статуса — второй видит документ не в UnderReview → CRM_DOCUMENT_STATUS_INVALID;
терминальный статус (Accepted/Rejected) переигрывается только новой записью того же Type (INV-DOC-4).
Альтернативные Потоки
A1. Прямая загрузка без предварительного запроса
- Инвестор загружает паспорт/скан по своей инициативе (US-INV-12): запись создаётся сразу в
Uploaded([*] → Uploaded), фаза 1 пропущена. - Публикуется
DocumentStatusChangedEvent(переход[*] → Uploaded), а неDocumentRequestedEvent.
Сходится с основным потоком на шаге 10 (далее проверка).
A2. Отклонён → повторная загрузка новой записью (INV-DOC-4)
- Документ в
Rejectedне редактируется и не переоткрывается. - Инвестор загружает исправленный файл — новой
DealDocumentEntityтого жеType(append-only история версий); проходит фазы 2–3 заново с собственнымDocumentId.
A3. Просрочка предоставления (worker)
- Документ остаётся
Requested,DueAt < now; таймлайн-событиеDocumentRequestedвInProgress. - Worker
timeline-overdue(идемпотентно — INV-TLE-4) переводит просроченные таймлайн-строкиInProgress → Overdue(../background/timeline-overdue.md). - Индикатор «просрочено» виден инвестору/агенту/застройщику (US-INV-09, US-AGT-06).
Постусловие
- Созданы/обновлены
DealDocumentEntityв целевых статусах (Requested/Uploaded/UnderReview/Accepted/Rejected); история версий append-only (отклонённые записи сохраняются). - В таймлайне сделки — события
DocumentRequested(закрыт вDoneпри загрузке) иDocumentAccepted/DocumentRejected. - В outbox — по одному
pin.crm.document-requested.v1на запрос иpin.crm.document-status-changed.v1на каждый переход статуса; чат-карточка и уведомления доставлены консюмерами (eventual). - Сделка и её статус (
DealStatus) не изменены — документооборот не мутирует агрегат сделки.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
CRM_DEAL_ALREADY_TERMINAL | 409 | Сделка Completed/Cancelled при запросе/загрузке (INV-DOC-5) | Запрос/загрузка недоступны для завершённой сделки |
CRM_DOCUMENT_UPLOADER_FORBIDDEN | 403 | Загружает не владелец Type (INV-DOC-1) | Скрыть «Загрузить» для чужого типа |
CRM_DOCUMENT_FILE_INVALID | 422 | MIME вне JPG/PDF/PNG или размер > 50 МБ (INV-DOC-3) | Показать требования к файлу, повтор |
CRM_DOCUMENT_STATUS_INVALID | 409 | Проверка документа не в UnderReview | Обновить список — статус изменён параллельно |
CRM_DOCUMENT_REVIEWER_FORBIDDEN | 403 | Принимает/отклоняет не сторона-проверяющий (INV-DOC-2) | Кнопки решения скрыты у неуполномоченного |
CRM_DOCUMENT_REASON_REQUIRED | 422 | Reject без RejectReason (INV-DOC-2) | Требовать причину отклонения |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
DocumentRequested | Фаза 1, [*] → Requested | Тот же обработчик: таймлайн DocumentRequested (InProgress+DueAt) |
DocumentUploaded | Фаза 2, Requested → Uploaded/UnderReview | Таймлайн DocumentUploaded (Done), закрытие парного DocumentRequested |
DocumentReviewed | Фаза 3, UnderReview → Accepted/Rejected | Таймлайн DocumentAccepted/DocumentRejected (INV-DOC-6) |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
DocumentRequestedEvent | pin.crm.document-requested.v1 | Фаза 1, на каждый запрошенный документ, через outbox | messenger (карточка «загрузите документы», US-INV-16), notifications — контракт ../events/document-requested.md |
DocumentStatusChangedEvent | pin.crm.document-status-changed.v1 | Фазы 2–3, на каждый переход Uploaded/UnderReview/Accepted/Rejected | notifications (статус документа инвестору, US-INV-14.3) — контракт ../events/document-status-changed.md |
Переход [*] → Requested публикует только document-requested; загрузка и решения — только
document-status-changed (разграничение — в обоих контрактах событий).
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Менеджер запрашивает документ → DealDocumentEntity(Requested) + таймлайн DocumentRequested(InProgress, DueAt) + одно pin.crm.document-requested.v1 с TargetParticipantUserId = активный инвестор | Интеграционный тест POST .../documents/request → 200; проверка записи, таймлайна и outbox |
| AC2 | Инвестор загружает валидный файл → Requested → Uploaded(→UnderReview для типа с проверкой), парный DocumentRequested закрыт в Done, pin.crm.document-status-changed.v1 опубликован | Тест POST .../upload → 200 + статусы и таймлайн |
| AC3 | Загрузка не владельцем типа → CRM_DOCUMENT_UPLOADER_FORBIDDEN (403); файл вне JPG/PDF/PNG или > 50 МБ → CRM_DOCUMENT_FILE_INVALID (422) | Негативные тесты INV-DOC-1/3 |
| AC4 | Reject без причины → CRM_DOCUMENT_REASON_REQUIRED (422); Accept/Reject не проверяющим → CRM_DOCUMENT_REVIEWER_FORBIDDEN (403) | Негативные тесты INV-DOC-2 |
| AC5 | Review(Accept/Reject) → терминальный статус + RejectReason при отклонении + pin.crm.document-status-changed.v1; notifications уведомляет инвестора (US-INV-14.3) | Тест POST .../confirm + консюмер-контракт |
| AC6 | Повторная загрузка после Rejected создаёт новую запись того же Type (INV-DOC-4), старая иммутабельна | Тест: две записи, Rejected без изменений |
| AC7 | Не загруженный к DueAt Requested → worker переводит таймлайн InProgress → Overdue идемпотентно | Тест worker timeline-overdue (INV-TLE-4) |
| AC8 | Скачивание отдаёт короткоживущую пресайн-ссылку только участнику сделки (US-INV-13) | Тест GET .../download + проверка участия |
Диаграмма
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы (будущие пути по конвенции):
../api/developer-request-contract-documents.md,../api/investor-upload-deal-document.md,../api/developer-confirm-contract-documents.md,../api/investor-download-deal-document.md; worker — ../background/timeline-overdue.md. - Domain entity: ../domain/deal-document.md (дом полей,
DealDocumentType/DealDocumentStatus, INV-DOC-1..6), ../domain/deal.md, ../domain/deal-timeline-event.md (типыDocument*,Overdue). - Kafka события: ../events/document-requested.md, ../events/document-status-changed.md.
- Соседние UC: UC-CRM-002; US-реестр — user-stories-deal.md (US-INV-12/13/14/16, US-DEV-03/05).
- ADR: 0052 tenancy, 0055 мини-мессенджер, 0056 simplicity-first.