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

Entity / Aggregate: DealDocumentEntity

Дом полей сущности «Документ сделки» контекста crm (вкладка «Документы» карточки сделки; US-INV-12/13/14/20, US-DEV-03/05). Payload pin.crm.document-requested.v1 и таймлайн-события документов ссылаются сюда.

Назначение

DealDocumentEntity — файл или запрос файла в рамках сделки: паспортные данные/сканы клиента, договор бронирования, чек оплаты, подписанный договор, документы для ДДУ/ДКП, сам договор ДДУ/ДКП. Живёт в двух режимах:

  • запрошен (Requested) — застройщик/система запросили документ у клиента (запрос документов для формирования договора ДДУ/ДКП); файла ещё нет;
  • загружен и проверяется — файл загружен стороной-владельцем типа, проходит проверку получателем (статусы Uploaded → UnderReview → Accepted/Rejected(+причина), US-INV-14).

Файлы хранятся во внешнем storage/CDN; в CRM — только метаданные и FileId.

Классификация

ПолеЗначение
Контекстcrm
KindEntity (в составе процесса сделки; отдельная таблица — жизненный цикл документа не блокирует правки сделки)
Source of truthcrm (метаданные); бинарь — storage/CDN
Таблицаcrm.deal_documents
Tenant isolationTenantId = DeveloperCompanyId (наследуется от сделки)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор документа.
TenantIdGuidДаindex ix_deal_documents_tenant_idиз сделкиЗастройщик сделки.
DealIdGuidДаFK fk_deal_documents_deal_id → crm.deals(id), index ix_deal_documents_deal_idappСделка (deal.md).
BookingIdGuid?НетFK fk_deal_documents_booking_id → crm.bookings(id), index where not nullappБронирование, к которому относится документ (типы Booking*).
TypeDealDocumentTypeДаenum как text, varchar(48), index (deal_id,type)appТип документа (см. enum).
StatusDealDocumentStatusДаenum как text, varchar(32), index ix_deal_documents_statusappСм. enum и stateDiagram.
TitlestringДаvarchar(256), не пустоеappОтображаемое название («Скан паспорта, стр. 1», «Договор бронирования №…»).
FileIdGuid?Нет (обязателен со статуса Uploaded, check ck_deal_documents_file)ссылка на storage (tenantId + fileId → presignedDownloadUrl)app (загрузка)Файл в storage/CDN; null — документ только запрошен.
FileNamestring?Нетvarchar(512)appОригинальное имя файла.
ContentTypestring?Нетvarchar(128); допустимые: application/pdf, image/jpeg, image/png (US-INV-12: JPG/PDF; конфигурация)appMIME-тип.
SizeByteslong?Нет> 0 and <= 52_428_800 (50 МБ, конфигурация)appРазмер файла.
RequisiteSetIdGuid?Нетлогическая ссылка на profileappНабор реквизитов, к которому относится документ (ТЗ.md:459: «бронь на мужа, покупка на жену / несколько собственников» — документы привязываются к конкретной персоне из profile).
RequestedByUserIdGuid?НетappКто запросил документ (менеджер застройщика/система); null — загружен без запроса.
RequestCommentstring?Нетvarchar(1000)appЧто именно нужно (текст карточки «загрузите документы» в чате, US-INV-16).
DueAtDateTimeOffset?НетappСрок предоставления (для индикации «просрочено» в таймлайне).
UploadedByUserIdGuid?НетappКто загрузил файл.
UploadedAtDateTimeOffset?НетappМомент загрузки (US-INV-13: дата у файла).
ReviewedByUserIdGuid?НетappКто принял/отклонил.
ReviewedAtDateTimeOffset?НетappМомент решения.
RejectReasonstring?Нетvarchar(1000); обязателен при Status = Rejected (check ck_deal_documents_reject_reason)appПричина отклонения (US-INV-14: «если документ отклонён — указана причина»).
CreatedAtDateTimeOffsetДаindex ix_deal_documents_created_atappСоздание записи (запрос или сразу загрузка).
CreatedByUserIdGuid?НетappИнициатор записи.
UpdatedAtDateTimeOffsetДаappДата последнего изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?НетappSoft-delete.
DeletedByUserIdGuid?НетappКто удалил.

Enum DealDocumentType

ЗначениеКто загружаетКто проверяетИсточник требования
ClientPassportИнвестор (или агент за него)Менеджер застройщикаUS-INV-12; ТЗ.md:397 (документы для бронирования).
BookingContractМенеджер застройщика— (клиент скачивает)ТЗ.md:399 («загрузка файла договора бронирования»).
SignedBookingContractИнвесторМенеджер застройщикаТЗ.md:455 («файл с подписанным договором»).
BookingPaymentProofИнвесторМенеджер застройщика (основание ConfirmPayment, booking.md)ТЗ.md:455 («файл с чеком об оплате»), ТЗ.md:401.
ContractDraftDocumentИнвесторМенеджер застройщикаТЗ.md:403–405 (документы для формирования ДДУ/ДКП).
SaleContractМенеджер застройщика— (клиент скачивает/подписывает)US-DEV-03; US-INV-20.
SignedSaleContractИнвесторМенеджер застройщикаподписанный ДДУ/ДКП.
PaymentProofИнвесторМенеджер застройщикаоплата по договору (эквайринг отложен — решение 9).
RosreestrDocumentМенеджер застройщикаТЗ.md:407 (подтверждение регистрации).
OtherЛюбой участникМенеджер застройщикаПрочие файлы сделки.

Enum DealDocumentStatus (US-INV-14: «загружено, на проверке, принято, отклонено»)

ЗначениеОписание
RequestedДокумент запрошен, файла нет (индикатор «Требуется загрузка», US-AGT-06/US-DEV-05).
UploadedФайл загружен («Загружено»). Для типов без проверки (загружает застройщик) — рабочий финал.
UnderReviewНа проверке у получателя.
AcceptedПринят. Терминальный.
RejectedОтклонён, RejectReason обязателен; клиент загружает новую версию — новой записью (история сохраняется). Терминальный.

Value objects

Value objects не используются.

Модели JSONB (versioned)

JSONB-модели не используются.

Связи

СвязьТипКлючПравило загрузкиDelete behavior
DealEntityreference (родитель)DealIdзагрузка при командах (guard статусов сделки)restrict
BookingEntityreferenceBookingIdзагрузка для booking-типовrestrict
Файл (storage/CDN)внешняяFileIdпресайн-ссылка на скачивание (tenantId+fileId)— (удаление файла — регламент storage)

Инварианты и переходы состояний

ИнвариантУсловиеКогда проверяетсяОшибка
INV-DOC-1Загружать файл типа может только сторона-владелец типа (таблица enum: инвестор/агент за него — клиентские типы; менеджер TenantId — застройщицкие)при загрузке файла (actor + роль участника сделки)CRM_DOCUMENT_UPLOADER_FORBIDDEN
INV-DOC-2Accepted/Rejected ставит только сторона-проверяющий; Rejected требует RejectReasonпри проверке документаCRM_DOCUMENT_REVIEWER_FORBIDDEN / CRM_DOCUMENT_REASON_REQUIRED
INV-DOC-3FileId/FileName/ContentType/SizeBytes обязательны со статуса Uploaded; тип/размер файла в допустимых пределахпри загрузке файла (check ck_deal_documents_file)CRM_DOCUMENT_FILE_INVALID
INV-DOC-4Повторная загрузка после Rejected — новая запись с тем же Type (append-only история версий); Rejected не редактируетсяпри загрузке файла
INV-DOC-5Документы создаются только у сделки в нетерминальном статусепри создании документаCRM_DEAL_ALREADY_TERMINAL
INV-DOC-6Смена статуса документа публикует уведомление (US-INV-14.3) и пишет таймлайн-событие в одном согласованном изменениипри загрузке/проверке документа

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
DocumentRequested / DocumentUploaded / DocumentReviewedдоменноесоответствующие переходытаймлайн-подписчик
DocumentRequestedEventинтеграционное, topic pin.crm.document-requested.v1, ContractVersion = 1[*] → RequestedDocumentId, DealId, TenantId, Type, Title, RequestComment, DueAt?, TargetUserId (кому адресован запрос — активный Investor сделки); консюмеры: messenger (карточка целевого действия «загрузите документы», US-INV-16), notifications; контракт — ../events/document-requested.md
DocumentStatusChangedEventинтеграционное, topic pin.crm.document-status-changed.v1, ContractVersion = 1Uploaded/Accepted/RejectedDocumentId, DealId, TenantId, Type, Status, RejectReason?; консюмеры: messenger (инфо-карточка), notifications (US-INV-14.3)

Интеграционные события публикуются атомарно со сменой статуса.

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK; ix_deal_documents_tenant_id; ix_deal_documents_deal_id; btree (deal_id,type); ix_deal_documents_status; index (booking_id) where not null; ix_deal_documents_created_at
Партиционирование / retentionnone; бинарные данные — вне БД (storage/CDN)
Concurrencyоптимистическая блокировка (правка не должна затирать чужую)
Permissions / PIIcrm.deal-documents.view / crm.deal-documents.manage / crm.deal-documents.review. Видимость — участники сделки (record-level как у deal.md); скачивание — пресайн-ссылка storage с проверкой участия. PII/Sensitive: ClientPassport и ContractDraftDocument содержат персональные данные — доступ только Investor-владелец, Agent сделки, SellManager, PinManager; в логи — только Id; пресайн-ссылки короткоживущие (правило storage).
Auditотметки создания/изменения/удаления (кто и когда); история версий — append-only записи; решения проверки — иммутабельные факты

Кросс-сервисные контракты (storage/CDN и downstream)

Данные/эффектКонтрагентКонтрактПоведение при отказе/устаревании
Бинарь файлавнешний storage/CDNв crm — только FileId + метаданные; скачивание — короткоживущая пресайн-ссылка (tenantId + fileId) с проверкой участиянедоступность storage не ломает метаданные/статусы в crm; ссылка выдаётся при запросе, срок жизни — правило storage
Карточка «загрузите документы»messengerDocumentRequestedEvent (pin.crm.document-requested.v1, key = DealId)at-least-once; консюмер идемпотентен по MessageId
Уведомление о смене статусаmessenger/notificationsDocumentStatusChangedEvent (pin.crm.document-status-changed.v1)at-least-once; идемпотентно по MessageId
Согласованность с storage

Загрузка/скачивание самого бинаря — вне транзакции crm: запись Uploaded фиксируется только после того, как FileId получен от storage; порядок «файл в storage → запись/статус в crm» гарантирует, что статус Uploaded не появляется без доступного файла.

Межсервисные последовательности и промежуточные состояния

Запрос документа. Менеджер застройщика (или система при формировании ДДУ/ДКП) создаёт запись Requested

  • строку таймлайна DocumentRequested (InProgress + DueAt) в одном согласованном изменении и публикует DocumentRequestedEventmessenger рисует карточку целевого действия «загрузите документы» адресату (TargetUserId = активный Investor сделки), notifications уведомляет. Клиент видит требование в таймлайне сразу (данные crm), карточку в чате — по мере доставки.

Загрузка и проверка. Владелец типа загружает файл (Requested → Uploaded, парно закрывает DocumentRequested → Done в таймлайне) → проверяющий переводит Uploaded → UnderReview → Accepted/Rejected. Каждая смена статуса публикует DocumentStatusChangedEvent (→ инфо-карточка messenger, notifications US-INV-14.3) и пишет таймлайн-строку в том же согласованном изменении (INV-DOC-6).

Повторная загрузка после отказа. Rejected не редактируется; клиент грузит новую версию новой записью того же Type (INV-DOC-4) — история версий append-only, downstream видит новую запись/статус.

Отказы, идемпотентность и конкуренция

  • Гонка загрузка ↔ проверка. Одновременные загрузка новой версии и проверка старой — оптимистическая блокировка по версии записи; переходы идут по своим записям (append-only версии), поэтому подмены не происходит.
  • Ролевые guard-ы. Загрузка — только сторона-владелец типа (INV-DOC-1) → CRM_DOCUMENT_UPLOADER_FORBIDDEN; проверка/Accepted/Rejected — только сторона-проверяющий (INV-DOC-2) → CRM_DOCUMENT_REVIEWER_FORBIDDEN; Rejected без причины → CRM_DOCUMENT_REASON_REQUIRED.
  • Валидность файла. Со статуса Uploaded обязательны FileId/FileName/ContentType/SizeBytes, тип и размер в допустимых пределах (INV-DOC-3) → иначе CRM_DOCUMENT_FILE_INVALID.
  • Терминальная сделка. Документы создаются только у сделки в нетерминальном статусе (INV-DOC-5) → иначе CRM_DEAL_ALREADY_TERMINAL.
  • Частичный отказ downstream. Метаданные и статус в crm — источник истины; недоступность messenger/notifications не откатывает смену статуса, события переотправляются (at-least-once).

Acceptance-критерии

  • Happy (запрос → загрузка → приём). Дано: сделка нетерминальна. Когда менеджер запрашивает документ, клиент грузит, менеджер принимает. Тогда записи Requested → Uploaded → Accepted, таймлайн DocumentRequested(Done)/DocumentAccepted, события DocumentRequestedEvent/DocumentStatusChangedEvent.
  • Загрузка чужого типа. Дано: инвестор пытается загрузить застройщицкий тип (BookingContract). Тогда отказ CRM_DOCUMENT_UPLOADER_FORBIDDEN.
  • Отклонение без причины. Дано: документ UnderReview. Когда проверяющий отклоняет без RejectReason. Тогда отказ CRM_DOCUMENT_REASON_REQUIRED.
  • Повторная загрузка после отказа. Дано: документ Rejected. Когда клиент грузит новую версию. Тогда создаётся новая запись того же Type, старая остаётся в истории.
  • Недопустимый файл. Дано: файл > 50 МБ или неподдерживаемого MIME. Когда его грузят. Тогда отказ CRM_DOCUMENT_FILE_INVALID.
  • PII-доступ. Дано: ClientPassport. Когда файл запрашивает участник вне {Investor-владелец, Agent, SellManager, PinManager}. Тогда пресайн-ссылка не выдаётся.

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

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

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

  • deal.md, booking.md, deal-timeline-event.md.
  • Реквизиты/персоны — docs/06-services/profile/README.md (наборы данных «муж/жена/дети»); storage/CDN — внешняя граница.