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

GET /api/investor/crm/deals/{dealId}/documents/{documentId}/download — Скачать документ сделки

Инвестор скачивает договор или иной документ своей сделки (US-INV-13: «скачать файл, у файла — дата»): операция возвращает короткоживущую пресайн-ссылку storage/CDN на бинарь по FileId документа. CRM хранит только метаданные и FileId; сам файл живёт во внешнем storage. Дом полей документа — deal-document.md.

Назначение

Дать участнику-инвестору по паре {dealId, documentId} временный URL для скачивания загруженного файла (договор бронирования, договор ДДУ/ДКП, чек и т. п.) без раскрытия постоянной ссылки на storage. Ссылка пресайнится на короткий срок и в логи не пишется. PII/Sensitive типы (ClientPassport, ContractDraftDocument) выдаются только владельцу-инвестору сделки (и Менеджеру ПИН), согласно deal-document.md.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areainvestor (JWT инвестора)
Feature groupdeal-documents
Статусdraft
Документdocs/06-services/crm/api/investor/download-deal-document.md

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

ПроверкаЗначение
Actor typeUser (Инвестор — участник-владелец сделки); Operator (Менеджер ПИН — platform-scope)
Auth policy / Permissionscrm.deal-documents.view (JWT)
Scope (RBAC)запрос не tenanted (Deal.TenantId = DeveloperCompanyId ≠ актор): чтение инвесторской стороны по явному record-level предикату (ADR-0052)
Record-levelактор — активный InvestorPrimary-участник сделки (deal.PrimaryInvestorUserId == ActorUserId, RemovedAt is null); для sensitive-типов — доп. правило INV-DOC
Tenant isolationTenantId пресайна = document.TenantId (DeveloperCompanyId), а не актора; передаётся в storage-адаптер
Auditread-sensitive (скачивание документа: DocumentId, DealId, ActorUserId, Type); DownloadUrl/FileName в логи не пишутся, только Id
Доступ

Запрос не tenanted — доступ проверяется record-level (ADR-0052): актор должен быть активным InvestorPrimary-участником сделки (deal.PrimaryInvestorUserId == ActorUserId, RemovedAt is null) либо PinManager (platform-scope, record-level фильтр снят). Для sensitive-типов документов (ClientPassport, ContractDraftDocument) действует дополнительное правило INV-DOC — доступ только {InvestorPrimary(владелец), Executor, SellManager, PinManager}.

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/investor/crm/deals/{dealId}/documents/{documentId}/download
Success status200 (тело — DownloadLinkModel; редирект не используется, ссылку применяет клиент)
Idempotency headerне применяется (GET — безопасный, семантически идемпотентный)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DealIdrouteGuidДа!= Guid.Empty
DocumentIdrouteGuidДа!= Guid.Empty; документ должен принадлежать DealId
ActorUserIdidentity claimsGuidДаJWT инвестораrecord-level (участник-владелец)
IsPinManageridentity claims (scope permission)boolДавычисляется из permission; снимает фильтр PrimaryInvestorUserId = actor (platform-scope)

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

Проекция storage-ответа + метаданные документа (дом полей — deal-document.md); дельта ниже. DownloadUrl — секрет короткого действия, не кэшируется и не логируется.

ПолеТипОбязательностьИсточникMaskingОписание
DocumentIdGuidДаDealDocumentEntity.IdИдентификатор документа.
Typestring (enum DealDocumentType)ДаTypeТип документа (enum строкой).
Statusstring (enum DealDocumentStatus)ДаStatusUploaded/UnderReview/Accepted/Rejected.
FileNamestringДаFileNameне логируетсяОригинальное имя файла (US-INV-13).
ContentTypestringДаContentTypeMIME (application/pdf/image/jpeg/image/png).
SizeByteslongДаSizeBytesРазмер файла.
UploadedAtDateTimeOffsetДаUploadedAtДата загрузки (US-INV-13: «дата у файла»).
DownloadUrlstringДапресайн storageне логируетсяПресайн-ссылка storage/CDN на скачивание.
ExpiresAtDateTimeOffsetДапресайн storageМомент истечения пресайн-ссылки (короткоживущая).
подсказка

DownloadUrl и FileName — секреты/PII короткого действия: не логируются, не кэшируются; пресайн-ссылка короткоживущая (ExpiresAt). Скачивание не создаёт новых записей и не меняет статус документа (INV-DOC-4 — новую версию порождает только повторная загрузка).

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (permission crm.deal-documents.view) либо Менеджер ПИН (platform-scope); ActorUserId, IsPinManager — из токена. Запрос не tenanted (у актора нет TenantId = DeveloperCompanyId сделки) — доступ режется record-level.
  2. Проверка входа и предусловий. DealId, DocumentId не пустые, иначе VALIDATION_FAILED. Существование, принадлежность, состояние и участие проверяются ниже.
  3. Что читаем. Документ по DocumentId; если его нет или удалён → CRM_DOCUMENT_NOT_FOUND. Принадлежность маршруту: document.DealId == DealId, иначе CRM_DOCUMENT_NOT_FOUND (документ чужой сделки не раскрывается тем же кодом). Сделку (с активными участниками, RemovedAt is null) — для проверки участия; если её нет или удалена → CRM_DEAL_NOT_FOUND.
  4. Бизнес-правила.
    • участие (record-level): IsPinManager или deal.PrimaryInvestorUserId == ActorUserId (актор — активный InvestorPrimary-участник); иначе FORBIDDEN (403) — чужая сделка не раскрывается;
    • состояние документа (Uploaded+): document.Status ∈ {Uploaded, UnderReview, Accepted, Rejected} (у всех есть FileId); статус Requested (файла нет) → CRM_DOCUMENT_NOT_UPLOADED (409);
    • PII/Sensitive (INV-DOC): если document.Type ∈ {ClientPassport, ContractDraftDocument} — доступ только ролям {InvestorPrimary(владелец), Executor, SellManager, PinManager} (deal-document.md); на этом endpoint актор — владелец-InvestorPrimary либо PinManager, оба проходят; иначе → CRM_DOCUMENT_SENSITIVE_FORBIDDEN (403);
    • маппинг метаданных DownloadLinkModel (DocumentId/Type/Status/FileName/ContentType/SizeBytes/ UploadedAt) — 1:1 из документа.
  5. Результат. Пресайн-ссылка запрашивается у storage/CDN по (document.TenantId, document.FileId) → {DownloadUrl, ExpiresAt}; сбой/таймаут storage → CRM_STORAGE_UNAVAILABLE (503). Ответ — DownloadLinkModel (метаданные шага 4 + DownloadUrl/ExpiresAt), 200. Файл и статусы не меняются (скачивание новой записи не создаёт, INV-DOC-4 — новую версию порождает только повторная загрузка).

Диаграмма

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

ТипДетали
Integration eventsnone (read-only GET; скачивание событий не публикует)
External callsstorage/CDN пресайн (tenantId + fileId → presignedDownloadUrl); timeout 3 c, retry 1; сбой → CRM_STORAGE_UNAVAILABLE
Cache / projections / searchnone (пресайн-ссылка короткоживущая, не кэшируется)
Notificationsnone

Ошибки и failure modes

Ошибки

Чтобы не раскрывать чужую сделку: отсутствие участия у актора отвечает 403 FORBIDDEN, а документ другой сделки (или несуществующий/удалённый) — 404 CRM_DOCUMENT_NOT_FOUND тем же кодом, без различия между «не существует» и «принадлежит другому dealId».

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400DealId/DocumentId = Guid.Empty«Некорректный идентификатор»no retry
FORBIDDEN403актор — не участник-владелец сделки«Нет доступа к документу»no retry
CRM_DOCUMENT_SENSITIVE_FORBIDDEN403sensitive-тип (ClientPassport/ContractDraftDocument), роль вне допустимого набора (INV-DOC)«Нет доступа к документу»no retry
CRM_DEAL_NOT_FOUND404сделка не найдена / удалена«Сделка не найдена»no retry
CRM_DOCUMENT_NOT_FOUND404документ не найден, удалён или не принадлежит DealId«Документ не найден»no retry
CRM_DOCUMENT_NOT_UPLOADED409Status = Requested (файл ещё не загружен)«Файл ещё не загружен»no retry (дождаться загрузки)
CRM_STORAGE_UNAVAILABLE503storage/CDN недоступен«Сервис файлов временно недоступен»retry

Acceptance Criteria

  • Given инвестор — активный InvestorPrimary-участник сделки, документ статуса Uploaded+ принадлежит DealId, When download, Then возвращается DownloadLinkModel с короткоживущей пресайн-ссылкой и ExpiresAt, статусы и записи документа не меняются (INV-DOC-4), ответ 200, DownloadUrl/FileName не логируются.
  • Given document.Status = Requested (файла нет), When download, Then CRM_DOCUMENT_NOT_UPLOADED (409).
  • Given актор не владелец сделки и не PinManager, When download, Then FORBIDDEN (403) — чужая сделка не раскрывается.
  • Given Type ∈ {ClientPassport, ContractDraftDocument} и роль актора вне допустимого набора, When download, Then CRM_DOCUMENT_SENSITIVE_FORBIDDEN (403) по INV-DOC.
  • Given document.DealId != DealId либо документ удалён/не существует, When download, Then CRM_DOCUMENT_NOT_FOUND (404); сделка отсутствует/удалена → CRM_DEAL_NOT_FOUND (404).
  • Given пресайн у storage/CDN не получен после 1 retry (таймаут 3 c), When download, Then CRM_STORAGE_UNAVAILABLE (503); клиент повторяет.
  • Given актор — PinManager (platform-scope), When download, Then record-level фильтр PrimaryInvestorUserId = actor снят, ссылка выдаётся при валидном документе.

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

  • Новые optional поля DownloadLinkModel — additive (без breaking change); удаление/переименование/смена типа — breaking (migration + rollout). Enum-поля (DealDocumentType/DealDocumentStatus) сериализуются строкой; клиент обязан толерантно обрабатывать неизвестное значение.
  • Logs: deal-document download requested (DocumentId, DealId, ActorUserId, Type; DownloadUrl/ FileName не пишутся — read-sensitive); metrics: crm_deal_document_download_total{result}, latency p95 (включая storage-вызов); traces — OTel, correlation через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь по конвенции).

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

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

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