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

UC-CRM-004. Документы сделки: запрос → загрузка → проверка → статусы

Назначение

Жизненный цикл документа сделки: менеджер застройщика запрашивает документ у клиента → инвестор (или агент за него) загружает файл → менеджер принимает/отклоняет → статусы и уведомления доходят до участников. Дом полей документа — ../domain/deal-document.md; здесь описаны только поток и дельта.

Metadata

ПолеЗначение
IDUC-CRM-004
Контекстcrm (см. docs/06-services/crm/)
Типосновной
Статусdraft

Актор

Сценарий двухсторонний: запрос/проверку выполняет застройщик, загрузку — клиентская сторона.

РольPermissions / scopeTenantRecord-level
Менеджер застройщика (запрос + проверка)crm.deal-documents.manage / .reviewTenantId = 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, enum DealStatus).
  • У сделки есть активный участник 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. Запрос документа (менеджер застройщика)

  1. Менеджер на вкладке «Документы» карточки сделки запрашивает документы для формирования договора ДДУ/ДКП. Модель запроса RequestContractDocumentsRequest { Items: RequestDocumentItem[] }, где RequestDocumentItem { Type: DealDocumentType, Title, RequestComment?, DueAt? }.
  2. Проверка входа. Items не пусты; Type/Title заданы; Type — клиентский.
  3. Проверки состояния. Сделка существует и нетерминальна (Status ∉ {Completed, Cancelled}, INV-DOC-5, иначе CRM_DEAL_ALREADY_TERMINAL); tenant совпадает (deal.TenantId == actor.TenantId); targetInvestorId = активный участник с ролью InvestorPrimary.
  4. Что меняется. Для каждого item создаётся DealDocumentEntity: Status = Requested, RequestedByUserId = actor, FileId = null; событие DocumentRequested. Пишется таймлайн DealTimelineEventEntity(DocumentRequested) (Status = InProgress, DueAt из item — INV-TLE-3). Всё — в одном согласованном изменении вместе с записью в outbox.
  5. Публикация. На каждый документ — DocumentRequestedEvent (топик pin.crm.document-requested.v1, TargetParticipantUserId = targetInvestorId) через outbox. Результат — список DocumentIds.
  6. Асинхронно. messenger рисует карточку целевого действия «загрузите документы» в чат сделки (US-INV-16), notifications шлёт «требуется загрузка».

Фаза 2. Загрузка файла (инвестор / агент за него)

  1. Инвестор выбирает файл; после заливки в storage вызывает POST /api/investor/crm/deals/{id}/documents/{docId}/upload с UploadDealDocumentRequest { FileId, FileName, ContentType, SizeBytes, RequisiteSetId? }.
  2. Проверка входа (INV-DOC-3). ContentType ∈ {application/pdf, image/jpeg, image/png}, SizeBytes ∈ (0; 52 428 800] — иначе CRM_DOCUMENT_FILE_INVALID.
  3. Проверки состояния. Документ в Status = Requested (или запись создаётся при прямой загрузке — A1); сделка нетерминальна (INV-DOC-5); INV-DOC-1 — actor является владельцем Type (инвестор/агент клиентских типов), иначе CRM_DOCUMENT_UPLOADER_FORBIDDEN.
  4. Переходы и что меняется. Requested → Uploaded; для типов с проверкой — далее → UnderReview (таблица владельцев ../domain/deal-document.md); UploadedByUserId/ UploadedAt проставлены. Пишется таймлайн DocumentUploaded (Status = Done, закрывает парный DocumentRequestedDone/CompletedAt, INV-TLE-3). Событие DocumentUploaded.
  5. Публикация. DocumentStatusChangedEvent (топик pin.crm.document-status-changed.v1, FromStatus = Requested, ToStatus = Uploaded|UnderReview) через outbox. Результат — DocumentId, Status.

Фаза 3. Проверка и статусы (менеджер застройщика)

  1. Менеджер видит документ UnderReview и принимает решение: POST /api/developer/crm/deals/{id}/documents/{docId}/confirm с ConfirmContractDocumentsRequest { Decision: DocumentReviewDecision (Accept|Reject), RejectReason? }.
  2. Проверки состояния. Документ в Status = UnderReview (иначе CRM_DOCUMENT_STATUS_INVALID); INV-DOC-2 — actor является стороной-проверяющим (SellManager/PinManager TenantId), иначе CRM_DOCUMENT_REVIEWER_FORBIDDEN; при Reject RejectReason обязателен, иначе CRM_DOCUMENT_REASON_REQUIRED.
  3. Переходы и что меняется. UnderReview → Accepted | Rejected (терминальные); ReviewedByUserId/ ReviewedAt, при отклонении RejectReason. Пишется таймлайн DocumentAccepted | DocumentRejected (Status = Done; при Rejected payload TimelineDocumentRejectedPayload.RejectReason). Событие DocumentReviewed.
  4. Публикация. DocumentStatusChangedEvent (ToStatus = Accepted|Rejected, RejectReason?) через outbox, атомарно с переходом. notifications уведомляет инвестора о статусе (US-INV-14.3).
  5. Скачивание (в любой момент). 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-ом статуса — второй видит документ не в UnderReviewCRM_DOCUMENT_STATUS_INVALID; терминальный статус (Accepted/Rejected) переигрывается только новой записью того же Type (INV-DOC-4).

Альтернативные Потоки

A1. Прямая загрузка без предварительного запроса

  1. Инвестор загружает паспорт/скан по своей инициативе (US-INV-12): запись создаётся сразу в Uploaded ([*] → Uploaded), фаза 1 пропущена.
  2. Публикуется DocumentStatusChangedEvent (переход [*] → Uploaded), а не DocumentRequestedEvent.

Сходится с основным потоком на шаге 10 (далее проверка).

A2. Отклонён → повторная загрузка новой записью (INV-DOC-4)

  1. Документ в Rejected не редактируется и не переоткрывается.
  2. Инвестор загружает исправленный файл — новой DealDocumentEntity того же Type (append-only история версий); проходит фазы 2–3 заново с собственным DocumentId.

A3. Просрочка предоставления (worker)

  1. Документ остаётся Requested, DueAt < now; таймлайн-событие DocumentRequested в InProgress.
  2. Worker timeline-overdue (идемпотентно — INV-TLE-4) переводит просроченные таймлайн-строки InProgress → Overdue (../background/timeline-overdue.md).
  3. Индикатор «просрочено» виден инвестору/агенту/застройщику (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_TERMINAL409Сделка Completed/Cancelled при запросе/загрузке (INV-DOC-5)Запрос/загрузка недоступны для завершённой сделки
CRM_DOCUMENT_UPLOADER_FORBIDDEN403Загружает не владелец Type (INV-DOC-1)Скрыть «Загрузить» для чужого типа
CRM_DOCUMENT_FILE_INVALID422MIME вне JPG/PDF/PNG или размер > 50 МБ (INV-DOC-3)Показать требования к файлу, повтор
CRM_DOCUMENT_STATUS_INVALID409Проверка документа не в UnderReviewОбновить список — статус изменён параллельно
CRM_DOCUMENT_REVIEWER_FORBIDDEN403Принимает/отклоняет не сторона-проверяющий (INV-DOC-2)Кнопки решения скрыты у неуполномоченного
CRM_DOCUMENT_REASON_REQUIRED422Reject без 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
DocumentRequestedEventpin.crm.document-requested.v1Фаза 1, на каждый запрошенный документ, через outboxmessenger (карточка «загрузите документы», US-INV-16), notifications — контракт ../events/document-requested.md
DocumentStatusChangedEventpin.crm.document-status-changed.v1Фазы 2–3, на каждый переход Uploaded/UnderReview/Accepted/Rejectednotifications (статус документа инвестору, 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
AC4Reject без причины → CRM_DOCUMENT_REASON_REQUIRED (422); Accept/Reject не проверяющим → CRM_DOCUMENT_REVIEWER_FORBIDDEN (403)Негативные тесты INV-DOC-2
AC5Review(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 + проверка участия

Диаграмма

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

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

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