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

POST /api/developer/crm/deals/{dealId}/sale-contract — Загрузить договор ДДУ/ДКП

Менеджер застройщика загружает подготовленный договор ДДУ/ДКП по сделке; клиент затем скачивает файл (US-DEV-03, US-INV-20), а подписанную застройщиком версию загружают тем же методом (US-DEV-07) — новой записью типа SaleContract (append-only история версий, INV-DOC-4). Создаёт запись-дочку агрегата сделки DealDocumentEntity сразу в статусе Uploaded (тип грузит застройщик, проверки клиентом нет — рабочий финал) и публикует DocumentStatusChangedEvent, по которому notifications уведомляет клиента «договор готов к скачиванию».

Назначение

Принять от менеджера застройщика метаданные загруженного в storage/CDN файла договора ДДУ/ДКП (FileId + имя/MIME/размер) и завести иммутабельную запись SaleContract у сделки, чтобы инвестор увидел документ на вкладке «Документы» карточки сделки и скачал по пресайн-ссылке.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areadeveloper
Feature groupdeal-documents
Статусdraft
Документdocs/06-services/crm/api/developer/upload-sale-contract.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (Менеджер застройщика)
Auth policy / Permissionscrm.deal-documents.manage (JWT identity)
Scope (RBAC)организация (застройщик) — TenantId = DeveloperCompanyId
Record-levelактор — активный участник сделки с ролью SellManager (или PinManager), сторона-владелец типа SaleContract — INV-DOC-1
Tenant isolationTenantId в каждом запросе (= DeveloperCompanyId сделки); разграничение применяется автоматически
Auditcreate (UploadedByUserId/CreatedByUserId = актор); сам файл договора — за пресайн-ссылкой storage, в логи метаданные без содержимого
Доступ

Permission crm.deal-documents.manage необходим, но недостаточен: дополнительно action проверяет record-level условие INV-DOC-1 — актор должен быть активным участником сделки с ролью SellManager или PinManager. Нарушение любого из двух условий → CRM_DOCUMENT_UPLOADER_FORBIDDEN (403).

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developer/crm/deals/{dealId}/sale-contract
Success status201 (создана новая запись документа)
Idempotency headerне применяется (append-only: каждая загрузка — новая версия/запись, INV-DOC-4)
Correlationсквозной trace через заголовок traceparent
подсказка

Метод append-only (INV-DOC-4): каждый вызов — новая запись SaleContract, а не перезапись предыдущей. Именно так подписанная застройщиком версия (US-DEV-07) добавляется тем же методом поверх ранее загруженного черновика, не удаляя историю.

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired (DeveloperCompanyId)Tenant isolation
DealIdrouteGuidДа!= Guid.Empty; существующая сделка TenantId (шаг 2)
FileIdbodyGuidДа!= Guid.Empty; ссылка на storage/CDN (tenantId + fileId → presignedDownloadUrl) — INV-DOC-3
FileNamebodystringДаtrim, 1..512оригинальное имя, не в логи целиком
ContentTypebodystringДаwhitelist application/pdf (договор — PDF; конфигурация MIME) — INV-DOC-3
SizeBytesbodylongДа> 0 and <= 52_428_800 (50 МБ, конфигурация) — INV-DOC-3
Titlebodystring?Нет0..256; default «Договор ДДУ/ДКП» по Deal.ContractType
RequisiteSetIdbodyGuid?Нетлогическая ссылка на profile (договор на конкретную персону/собственника)

Тип документа фиксирован системой (DealDocumentType.SaleContract) — не входной параметр. ContentType — строгий whitelist (не голый MIME), enum-статусы и типы — строкой. Дом полей — deal-document.md.

Модель ответа DealDocumentModel

Проекция созданной записи DealDocumentEntity (здесь только возвращаемая дельта):

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаDealDocumentEntity.IdИдентификатор новой записи документа.
DealIdGuidДаDealDocumentEntity.DealIdСделка (deal.md).
TypeDealDocumentTypeДаDealDocumentEntity.Type (enum строкой)Всегда SaleContract.
StatusDealDocumentStatusДаDealDocumentEntity.Status (enum строкой)Всегда Uploaded (тип без проверки, рабочий финал).
TitlestringДаDealDocumentEntity.TitleОтображаемое название на вкладке «Документы».
FileIdGuidДаDealDocumentEntity.FileIdФайл в storage/CDN (скачивание — пресайн-ссылка).
FileNamestringДаDealDocumentEntity.FileNameОригинальное имя файла.
ContentTypestringДаDealDocumentEntity.ContentTypeMIME (application/pdf).
SizeByteslongДаDealDocumentEntity.SizeBytesРазмер файла.
RequisiteSetIdGuid?НетDealDocumentEntity.RequisiteSetIdПерсона/собственник (profile), если задан.
UploadedByUserIdGuidДаDealDocumentEntity.UploadedByUserIdМенеджер застройщика — загрузчик.
UploadedAtDateTimeOffsetДаDealDocumentEntity.UploadedAtМомент загрузки.
CreatedAtDateTimeOffsetДаDealDocumentEntity.CreatedAtСоздание записи.

Прежние версии не затираются (INV-DOC-4): полную историю SaleContract отдаёт список документов сделки.

Алгоритм

  1. Контекст и доступ. Выполняет менеджер застройщика (User, permission crm.deal-documents.manage, иначе CRM_DOCUMENT_UPLOADER_FORBIDDEN); tenant — TenantId = DeveloperCompanyId. Идемпотентность не применяется — загрузка append-only (повтор = новая запись версии, INV-DOC-4).
  2. Проверка входа и предусловий.
    • DealId/FileId != Guid.Empty; FileName 1..512; ContentType == "application/pdf"; SizeBytes ∈ 1..52_428_800; Title 0..256 (INV-DOC-3) — иначе VALIDATION_FAILED;
    • сделка существует в tenant застройщика — иначе CRM_DEAL_NOT_FOUND (чужой tenant не раскрывается);
    • допустимость состояния: Status ∉ {Completed, Cancelled} (нетерминальная, INV-DOC-5) — иначе CRM_DEAL_ALREADY_TERMINAL;
    • право загрузки (INV-DOC-1): актор — активный участник сделки с ролью SellManager/PinManager (сторона-владелец типа SaleContract) — иначе CRM_DOCUMENT_UPLOADER_FORBIDDEN.
  3. Что читаем. Сделку (для guard статуса/tenant) и её активных участников (RemovedAt is null — проверка роли загрузчика). Файл в crm не читается — доверяем FileId (валидность бинаря/антивирус — регламент storage).
  4. Бизнес-правила и переходы (INV-DOC-1/3). Создаётся новая запись SaleContract в статусе Uploaded (переход [*] → Uploaded, тип грузит застройщик — без фазы Requested и без Review), маппинг: TenantId = deal.TenantId, DealId, Type = SaleContract, FileId/FileName/ContentType/SizeBytes из тела, Title = входной ?? default по Deal.ContractType, RequisiteSetId?, UploadedByUserId = CreatedByUserId = актор, UploadedAt = CreatedAt = now. Инвариант INV-DOC-3 (наличие FileId/имени/MIME/размера со статуса Uploaded) — доменный guard + БД-check ck_deal_documents_file. Фиксируется доменный факт DocumentUploaded; подписчик пишет строку таймлайна сделки (INV-DOC-6).
  5. Что меняется. Появляется новая запись документа SaleContract (история версий не затирается, INV-DOC-4); в ленту добавляется строка события. Сама сделка не меняется (документ — отдельная таблица).
  6. События. Интеграционное DocumentStatusChangedEvent → топик pin.crm.document-status-changed.v1 (ContractVersion = 1, AggregateId = DocumentId, AggregateType = "DealDocument"); payload {DocumentId, DealId, TenantId, DocumentType = SaleContract, FromStatus, ToStatus = Uploaded, ChangedByUserId = UploadedByUserId, ChangedAt = UploadedAt} (переход [*] → Uploaded) — через outbox (дом — document-status-changed.md). Потребитель notifications уведомляет инвестора сделки «договор ДДУ/ДКП готов к скачиванию»; синхронно crm уведомление не шлёт.
  7. Согласованность. Запись документа, строка таймлайна и событие фиксируются в одном согласованном изменении (атомарно, INV-DOC-6).
  8. Результат. Проекция созданной записи DealDocumentModel → HTTP 201. Sensitive-полей в ответе нет (сам файл — за пресайн-ссылкой storage).

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsDocumentStatusChangedEventpin.crm.document-status-changed.v1 (ToStatus = Uploaded, Type = SaleContract), outbox
External callsnone синхронно (файл уже в storage/CDN; валидность бинаря — регламент storage); скачивание клиентом — пресайн-ссылка storage
Cache / projections / searchnone (deal_documents — прямая запись; агрегат сделки не трогается)
Notificationsопосредованно: notifications — консюмер document-status-changed (клиенту «договор готов к скачиванию»)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400FileId/DealId пустые, ContentType вне whitelist, SizeBytes вне 1..50 МБ, длины имён«Проверьте параметры загрузки»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
CRM_DOCUMENT_UPLOADER_FORBIDDEN403нет crm.deal-documents.manage, чужой TenantId или актор не SellManager/PinManager сделки (INV-DOC-1)«Недостаточно прав для загрузки договора»no retry
CRM_DEAL_NOT_FOUND404сделка по DealId не найдена в TenantId«Сделка не найдена»no retry
CRM_DEAL_ALREADY_TERMINAL409Deal.Status ∈ {Completed, Cancelled} (INV-DOC-5)«Сделка завершена или отменена»no retry
CRM_DOCUMENT_FILE_INVALID422доменный guard INV-DOC-3 / нарушение ck_deal_documents_file (defense-in-depth)«Файл договора недопустим»no retry
Ошибки

Все коды — no retry: 400/422 требуют исправить входные данные (метаданные файла), 403/404/409 — доступ или состояние сделки, повтор того же запроса результат не изменит.

Acceptance Criteria

  • Given нетерминальная сделка в TenantId и актор — активный участник SellManager/PinManager, When загружается валидный PDF, Then создана новая запись SaleContract (Uploaded), добавлена строка таймлайна, событие DocumentStatusChangedEventpin.crm.document-status-changed.v1 (ToStatus = Uploaded, Type = SaleContract) в outbox, ответ 201.
  • Given по сделке уже есть запись SaleContract, When повторная загрузка (например, подписанной застройщиком версии, US-DEV-07), Then прежняя запись не затирается — создаётся новая (INV-DOC-4); полную историю отдаёт список документов сделки.
  • Given Deal.Status ∈ {Completed, Cancelled}, When upload, Then CRM_DEAL_ALREADY_TERMINAL (409) по INV-DOC-5.
  • Given актор не SellManager/PinManager сделки, чужой TenantId либо нет crm.deal-documents.manage, When upload, Then CRM_DOCUMENT_UPLOADER_FORBIDDEN (403) по INV-DOC-1.
  • Given сделки по DealId нет в TenantId, When upload, Then CRM_DEAL_NOT_FOUND (404) — существование чужой сделки не раскрывается.
  • Given ContentType вне whitelist application/pdf, SizeBytes ∉ [1, 50 МБ] или пустые FileId/DealId, When upload, Then VALIDATION_FAILED (400); нарушение доменного guard / ck_deal_documents_fileCRM_DOCUMENT_FILE_INVALID (422) по INV-DOC-3.
  • Given успешная загрузка, When событие document-status-changed доставлено, Then потребитель notifications уведомляет инвестора «договор ДДУ/ДКП готов к скачиванию»; синхронно crm уведомление не шлёт.

Совместимость и наблюдаемость

  • Новые optional поля тела/ответа — additive (без breaking); удаление/переименование/смена типа поля или расширение whitelist ContentType — breaking (migration + rollout). Enum наружу — строкой (SaleContract, Uploaded); дом значений — deal-document.md.
  • Logs: sale-contract uploaded (DealId, DocumentId, TenantId, SizeBytes; FileName — усечённо, без содержимого файла); metrics: crm_sale_contract_upload_total{result}, latency p95 (< 500 мс, SLO команд); traces — OTel, correlation через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь по конвенции).

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

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

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

  • Сущности: deal-document.md (дом полей/статусов, DealDocumentType.SaleContract, INV-DOC-1/3/4/5/6, ck_deal_documents_file), deal.md (DealStatus, ContractType, участники/роли).
  • Событие: document-status-changed.md (payload/consumers, pin.crm.document-status-changed.v1).
  • Контекст: README; tenancy — ADR-0052.