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

GET /api/investor/crm/deals/{dealId} — Карточка сделки (инвестор)

Read-модель карточки сделки со стороны инвестора-покупателя (/profile/deals/[dealId], вкладки «Условия/Документы/О объекте недвижимости/О проекте»): инвестор открывает свою сделку и видит объект и полную стоимость, статус, платное бронирование, банк/эскроу, тип договора и сводку своих документов (US-INV-02..08, US-INV-22). Без фиксаций (инвестор не видит фиксаций и агентской «кухни», README «Ответственность»). Только чтение: дом полей агрегата — deal.md; здесь описана проекция InvestorDealCardModel и алгоритм её сборки. «Ход сделки» (таймлайн) и полный список документов — отдельные постраничные методы (см. «Побочные эффекты»).

Назначение

Дать инвестору карточку одной своей сделки по dealId: заголовок и коарс-статус (US-INV-03), дату последнего обновления (US-INV-04), объект и полную стоимость объекта недвижимости (US-INV-05), сумму и статус платного бронирования (US-INV-06), банк и статус эскроу (US-INV-07/19, номер счёта — маска ****NNNN), тип договора ДДУ/ДКП (US-INV-08), отметку «Ознакомлен» (US-INV-18), готовность чата сделки (US-INV-22) и сводку своих документов с их статусами (US-INV-12/13/14). Вкладка «Условия» = финансовая сводка ниже; «Документы» = сводка Documents; «О объекте недвижимости»/«О проекте» — данные chessboard/catalog, отдельные запросы.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areainvestor
Feature groupdeals
Статусdraft
Документdocs/06-services/crm/api/investor/get-deal.md

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

ПроверкаЗначение
Actor typeUser (Инвестор — только свои сделки)
Auth policy / Permissionscrm.deals.view (JWT инвестора); фактический доступ ограничен record-level self
Scope (RBAC)самообслуживание: инвестор видит только записи с PrimaryInvestorUserId = actor (не organization-scope)
Record-levelDealEntity.PrimaryInvestorUserId = ActorUserId — участник-покупатель; чужая/несуществующая сделка → CRM_DEAL_NOT_FOUND
Tenant isolationЗапрос не tenanted (Deal.TenantId = DeveloperCompanyId ≠ актор): чтение по явному record-level предикату PrimaryInvestorUserId = ActorUserId (ADR-0052)
Auditread; EscrowAccountNumber — Sensitive: наружу маска ****NNNN, в логи запрещён
Доступ

Инвестор видит только свои сделки (PrimaryInvestorUserId = ActorUserId) — не organization-scope и не tenant-scope застройщика. Фиксации, DealMemberEntity и клиентская карточка агентства инвестору не раскрываются. Полный номер эскроу-счёта наружу не отдаётся — только маска ****NNNN.

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DealIdroute {dealId}GuidДа!= Guid.Empty
ActorUserIdidentity claimsGuidДаJWT инвестораrecord-level (PrimaryInvestorUserId)

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

Проекция агрегата; дом полей — deal.md (VO Money; enum DealStatus/DealContractType/ PaymentMethod/EscrowStatus/DealChatStatus; группа этапа — pipeline-step.md). Ниже — только дельта инвесторской проекции; все enum сериализуются строкой.

ПолеТипОбязательностьИсточникMaskingОписание
DealIdGuidДаIdИдентификатор сделки (dealId маршрута).
TitlestringДаTitleЗаголовок карточки («Объект №…, ЖК …»).
StatusDealStatusДаStatusApplication/Booking/Sale/Completed/Cancelled (US-INV-03).
StepGroupPipelineStepGroupДаStepGroupКоарс-стадия воронки для покупателя (кастомный StepId застройщика наружу не отдаём).
PropertyObjectIdGuidДаPropertyObjectIdОбъект сделки (chessboard); display-данные — отдельный запрос вкладки «О объекте недвижимости».
ProjectIdGuidДаProjectIdПроект объекта (вкладка «О проекте»).
BuildingIdGuid?НетBuildingIdЗдание/литер.
FinanceInvestorDealFinanceModelДаcomputed (см. ниже)Вкладка «Условия»: цена, оплата, договор, банк/эскроу.
CurrentBookingInvestorBookingModel?Нетбронь по BookingIdТекущая бронь (US-INV-06); null — активной брони нет.
DocumentsInvestorDocumentsSummaryModelДаcomputed по crm.deal_documentsСводка вкладки «Документы» (статусы + индикатор «загрузите», US-INV-16).
ChatStatusDealChatStatusДаChatStatusГотовность чата сделки (проекция messenger, US-INV-22, ADR-0055).
TermsAcknowledgedAtDateTimeOffset?НетTermsAcknowledgedAtОтметка инвестора «Ознакомлен» (US-INV-18).
CancelReasonstring?НетCancelReasonПричина отказа (при Status = Cancelled).
CreatedAtDateTimeOffsetДаCreatedAtСоздание сделки.
UpdatedAtDateTimeOffsetДаUpdatedAtДата последнего обновления (US-INV-04).

InvestorDealFinanceModel

ПолеТипОбязательностьИсточникMaskingОписание
PriceMoney (owned: Amount,Currency)ДаDeal.Price (snapshot chessboard)Полная стоимость объекта недвижимости (US-INV-05).
PaymentMethodPaymentMethod? (enum→string)НетDeal.PaymentMethodСпособ оплаты (purchase-program.md).
InstallmentPresetIdGuid?НетDeal.InstallmentPresetIdПресет рассрочки (при PaymentMethod = Installment).
ContractTypeDealContractType? (enum→string)НетDeal.ContractTypeDdu/Dkp (US-INV-08).
BankIdGuid?НетDeal.BankIdБанк сделки/эскроу; null → UI «Не указан» (US-INV-07).
EscrowStatusEscrowStatus (enum→string)ДаDeal.EscrowStatusNone/Provided («Эскроу забронирован», US-INV-19).
EscrowAccountNumberstring?НетDeal.EscrowAccountNumberмаска ****NNNN (последние 4)Эскроу-счёт; полный номер инвестору не отдаётся — только SellManager/PinManager (deal.md).

InvestorBookingModel

Дом — booking.md; проекция текущей брони (US-INV-06/10).

ПолеТипОбязательностьИсточникОписание
BookingIdGuidДаBookingEntity.IdИдентификатор брони.
StatusBookingStatus (enum→string)ДаStatusReserved/AwaitingPayment/Booked/…
ProgramNamestringДаProgramTerms.NameНазвание программы бронирования (snapshot).
IsPaidboolДаProgramTerms.IsPaidПлатная ли программа.
PaymentStatusBookingPaymentStatus (enum→string)ДаPaymentStatusNotRequired/Pending/Paid (US-INV-06).
PaymentAmountMoney? (owned)НетPaymentAmountСумма платного бронирования (US-INV-06).
PaidAtDateTimeOffset?НетPaidAtДата подтверждения оплаты («Оплачено» + дата).
ReserveExpiresAtDateTimeOffsetДаReserveExpiresAtДедлайн резерва.
BookingExpiresAtDateTimeOffset?НетBookingExpiresAtДедлайн подтверждённой брони.

InvestorDocumentsSummaryModel

Дом — deal-document.md; скалярная сводка вкладки по документам, видимым инвестору-участнику (record-level как в deal-document.md). Полный список — отдельный постраничный метод.

ПолеТипОбязательностьИсточникОписание
RequiresUploadboolДаRequestedCount > 0Индикатор «загрузите документы» (US-INV-16).
RequestedCountintДаCount(Status = Requested)Запрошено, файла нет.
UploadedCountintДаCount(Status ∈ {Uploaded, UnderReview})Загружено / на проверке (US-INV-13).
AcceptedCountintДаCount(Status = Accepted)Принято (US-INV-14).
RejectedCountintДаCount(Status = Rejected)Отклонено — нужна повторная загрузка (US-INV-14).

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор с permission crm.deals.view; ActorUserId — из токена. Запрос не tenanted (у инвестора нет TenantId = DeveloperCompanyId сделки).
  2. Проверка входа и предусловий. DealId != Guid.Empty, иначе VALIDATION_FAILED (400). Существование и принадлежность — при загрузке; чтение разрешено во всех статусах, включая терминальные Completed/Cancelled.
  3. Что читаем.
    • сделку — строго по record-level предикату Id == DealId && PrimaryInvestorUserId == ActorUserId && DeletedAt == null (scope-bypass developer-tenant, ADR-0052). Нет совпадения → CRM_DEAL_NOT_FOUND (чужую/несуществующую/удалённую сделку не раскрываем). Фиксации, DealMemberEntity и клиентская карточка агентства не читаются;
    • текущую бронь: если deal.BookingId != null — бронь по BookingId (проекция в InvestorBookingModel); иначе null;
    • сводку документов: скалярные счётчики по статусам документов сделки, видимых инвестору (без выборки строк) → InvestorDocumentsSummaryModel; RequiresUpload = RequestedCount > 0. Полный список документов и таймлайн здесь не загружаются (отдельные вкладки).
  4. Бизнес-правила и маппинг. Переходов состояний нет. Маппинг поле-в-поле dealInvestorDealCardModel + Finance/CurrentBooking/Documents (таблицы выше); Money → owned Amount/Currency. Маскирование эскроу: Finance.EscrowAccountNumber = ****NNNN (последние 4 цифры; полный номер инвестору не отдаётся — deal.md). Enum-поля сериализуются строкой.
  5. Результат. InvestorDealCardModel → HTTP 200. Чтение согласовано на момент запроса (агрегат + бронь
    • счётчики документов; ChatStatus — eventual-проекция из messenger). Ошибки → 404/400.

Диаграмма

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

ТипДетали
Integration eventsnone (read-only GET)
External callsnone в самом методе (display объекта недвижимости/проекта/банка — отдельные запросы к chessboard/catalog)
Cache / projections / searchnone (прямое чтение агрегата, брони, скалярных счётчиков документов)
Notificationsnone
Отдельные запросы вкладок«Ход сделки» — таймлайн (get-deal-timeline.md); полный список документов — постраничный метод по DealId. В этот ответ не входят.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400DealId = Guid.Empty«Некорректный идентификатор сделки»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
FORBIDDEN403нет permission crm.deals.view«Доступ запрещён»no retry
CRM_DEAL_NOT_FOUND404сделки нет, PrimaryInvestorUserId не совпал или удалена«Сделка не найдена»no retry
Ошибки

Чужая сделка намеренно даёт 404, а не 403: существование сделки другого инвестора не раскрывается.

Acceptance Criteria

  • Given своя сделка, When инвестор запрашивает карточку, Then возвращается InvestorDealCardModel с Finance, CurrentBooking?, Documents; EscrowAccountNumber — маска ****NNNN; кастомный StepId наружу не отдаётся (только StepGroup); ответ 200.
  • Given deal.BookingId == null, When запрос, Then CurrentBooking = null, остальная карточка заполнена.
  • Given Status ∈ {Completed, Cancelled}, When запрос, Then карточка читается, при Cancelled заполнен CancelReason.
  • Given PrimaryInvestorUserId != actor, сделка удалена или не существует, When запрос, Then CRM_DEAL_NOT_FOUND (404) — а не 403 (существование чужой сделки не раскрывается).
  • Given нет permission crm.deals.view, When запрос, Then FORBIDDEN (403); пустой DealIdVALIDATION_FAILED (400).
  • Given у сделки есть документы в статусе Requested, When запрос, Then Documents.RequiresUpload = true и счётчики (RequestedCount/UploadedCount/AcceptedCount/RejectedCount) соответствуют статусам; полный список документов и таймлайн в ответ не входят (отдельные методы).
  • Given проекция чата из messenger (ADR-0055) отстаёт, When запрос, Then ChatStatus — eventual-проекция; допустима микрорассинхронизация с прочими полями, консистентными на момент запроса.

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

  • Новые optional поля InvestorDealCardModel/подмоделей — additive (без breaking change); удаление/ переименование/смена типа — breaking (migration + rollout). Enum-поля (DealStatus/PaymentMethod/ DealContractType/EscrowStatus/BookingStatus/BookingPaymentStatus/…) сериализуются строкой; клиент обязан толерантно обрабатывать неизвестное значение.
  • Logs: investor deal-card viewed (DealId, PrimaryInvestorUserId; EscrowAccountNumber не пишется); metrics: crm_investor_operations_total{operation="get-deal",result}, latency p95 (< 500 мс, SLO чтений README); traces — OTel, correlation через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь по конвенции).

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

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

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