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 |
| Kind | Entity (в составе процесса сделки; отдельная таблица — жизненный цикл документа не блокирует правки сделки) |
| Source of truth | crm (метаданные); бинарь — storage/CDN |
| Таблица | crm.deal_documents |
| Tenant isolation | TenantId = DeveloperCompanyId (наследуется от сделки) |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор документа. |
TenantId | Guid | Да | index ix_deal_documents_tenant_id | из сделки | Застройщик сделки. |
DealId | Guid | Да | FK fk_deal_documents_deal_id → crm.deals(id), index ix_deal_documents_deal_id | app | Сделка (deal.md). |
BookingId | Guid? | Нет | FK fk_deal_documents_booking_id → crm.bookings(id), index where not null | app | Бронирование, к которому относится документ (типы Booking*). |
Type | DealDocumentType | Да | enum как text, varchar(48), index (deal_id,type) | app | Тип документа (см. enum). |
Status | DealDocumentStatus | Да | enum как text, varchar(32), index ix_deal_documents_status | app | См. enum и stateDiagram. |
Title | string | Да | varchar(256), не пустое | app | Отображаемое название («Скан паспорта, стр. 1», «Договор бронирования №…»). |
FileId | Guid? | Нет (обязателен со статуса Uploaded, check ck_deal_documents_file) | ссылка на storage (tenantId + fileId → presignedDownloadUrl) | app (загрузка) | Файл в storage/CDN; null — документ только запрошен. |
FileName | string? | Нет | varchar(512) | app | Оригинальное имя файла. |
ContentType | string? | Нет | varchar(128); допустимые: application/pdf, image/jpeg, image/png (US-INV-12: JPG/PDF; конфигурация) | app | MIME-тип. |
SizeBytes | long? | Нет | > 0 and <= 52_428_800 (50 МБ, конфигурация) | app | Размер файла. |
RequisiteSetId | Guid? | Нет | логическая ссылка на profile | app | Набор реквизитов, к которому относится документ (ТЗ.md:459: «бронь на мужа, покупка на жену / несколько собственников» — документы привязываются к конкретной персоне из profile). |
RequestedByUserId | Guid? | Нет | — | app | Кто запросил документ (менеджер застройщика/система); null — загружен без запроса. |
RequestComment | string? | Нет | varchar(1000) | app | Что именно нужно (текст карточки «загрузите документы» в чате, US-INV-16). |
DueAt | DateTimeOffset? | Нет | — | app | Срок предоставления (для индикации «просрочено» в таймлайне). |
UploadedByUserId | Guid? | Нет | — | app | Кто загрузил файл. |
UploadedAt | DateTimeOffset? | Нет | — | app | Момент загрузки (US-INV-13: дата у файла). |
ReviewedByUserId | Guid? | Нет | — | app | Кто принял/отклонил. |
ReviewedAt | DateTimeOffset? | Нет | — | app | Момент решения. |
RejectReason | string? | Нет | varchar(1000); обязателен при Status = Rejected (check ck_deal_documents_reject_reason) | app | Причина отклонения (US-INV-14: «если документ отклонён — указана причина»). |
CreatedAt | DateTimeOffset | Да | index ix_deal_documents_created_at | app | Создание записи (запрос или сразу загрузка). |
CreatedByUserId | Guid? | Нет | — | app | Инициатор записи. |
UpdatedAt | DateTimeOffset | Да | — | app | Дата последнего изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | — | app | Soft-delete. |
DeletedByUserId | Guid? | Нет | — | 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 |
|---|---|---|---|---|
DealEntity | reference (родитель) | DealId | загрузка при командах (guard статусов сделки) | restrict |
BookingEntity | reference | BookingId | загрузка для booking-типов | restrict |
| Файл (storage/CDN) | внешняя | FileId | пресайн-ссылка на скачивание (tenantId+fileId) | — (удаление файла — регламент storage) |
Инварианты и переходы состояний
| Инвариант | Условие | Когда проверяется | Ошибка |
|---|---|---|---|
| INV-DOC-1 | Загружать файл типа может только сторона-владелец типа (таблица enum: инвестор/агент за него — клиентские типы; менеджер TenantId — застройщицкие) | при загрузке файла (actor + роль участника сделки) | CRM_DOCUMENT_UPLOADER_FORBIDDEN |
| INV-DOC-2 | Accepted/Rejected ставит только сторона-проверяющий; Rejected требует RejectReason | при проверке документа | CRM_DOCUMENT_REVIEWER_FORBIDDEN / CRM_DOCUMENT_REASON_REQUIRED |
| INV-DOC-3 | FileId/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 | [*] → Requested | DocumentId, 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 = 1 | Uploaded/Accepted/Rejected | DocumentId, 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 |
| Партиционирование / retention | none; бинарные данные — вне БД (storage/CDN) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую) |
| Permissions / PII | crm.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 |
| Карточка «загрузите документы» | messenger | DocumentRequestedEvent (pin.crm.document-requested.v1, key = DealId) | at-least-once; консюмер идемпотентен по MessageId |
| Уведомление о смене статуса | messenger/notifications | DocumentStatusChangedEvent (pin.crm.document-status-changed.v1) | at-least-once; идемпотентно по MessageId |
Загрузка/скачивание самого бинаря — вне транзакции crm: запись Uploaded фиксируется только после того,
как FileId получен от storage; порядок «файл в storage → запись/статус в crm» гарантирует, что статус
Uploaded не появляется без доступного файла.
Межсервисные последовательности и промежуточные состояния
Запрос документа. Менеджер застройщика (или система при формировании ДДУ/ДКП) создаёт запись Requested
- строку таймлайна
DocumentRequested(InProgress+DueAt) в одном согласованном изменении и публикуетDocumentRequestedEvent→messengerрисует карточку целевого действия «загрузите документы» адресату (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}. Тогда пресайн-ссылка не выдаётся.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (4): Entity / Aggregate: BookingEntity, Entity / Aggregate: DealEntity, Entity / Aggregate: DealTimelineEventEntity (TPH), Entity / Aggregate: MessageEntity (TPH: Text/Attachment/EventCard)
- API (16): GET /api/agent/crm/deals/{dealId} — Карточка сделки (агент), GET /api/developer/crm/deals/{dealId} — Карточка сделки (застройщик), GET /api/investor/crm/deals/{dealId} — Карточка сделки (инвестор), GET /api/investor/crm/deals/{dealId}/documents/{documentId}/download — Скачать документ сделки, GET /api/investor/crm/deals/{dealId}/timeline — Таймлайн сделки (инвестор), GET /api/pin-manager/crm/deals/{dealId} — Карточка любой сделки (Менеджер ПИН), POST /api/developer/crm/bookings/{bookingId}/confirm-documents — Подтвердить документы брони, POST /api/developer/crm/bookings/{bookingId}/confirm-payment — Подтвердить оплату брони, POST /api/developer/crm/bookings/{bookingId}/contract — Загрузить договор брони, POST /api/developer/crm/deals/{dealId}/contracts-prepared — Отметить «Договоры подготовлены», POST /api/developer/crm/deals/{dealId}/documents/{documentId}/confirm — Подтвердить документы ДДУ/ДКП, POST /api/developer/crm/deals/{dealId}/documents/request — Запросить документы ДДУ/ДКП (+4)
- Use Cases (4): UC-BKG-004. Оплата платного бронирования (чек + подтверждение), UC-CRM-004. Документы сделки: запрос → загрузка → проверка → статусы, UC-CRM-005. Оформление сделки: договоры → эскроу → Росреестр → завершение, US-реестр блока «Сделка» (US-INV / US-AGT / US-DEV / US-MGR)
- События (2): Event Contract: pin.crm.document-requested (DocumentRequestedEvent), Event Contract: pin.crm.document-status-changed (DocumentStatusChangedEvent)
- index.md (1): CRM Service
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- deal.md, booking.md, deal-timeline-event.md.
- Реквизиты/персоны —
docs/06-services/profile/README.md(наборы данных «муж/жена/дети»); storage/CDN — внешняя граница.