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

GET /api/v1/portfolio/properties/{propertyId} — Карточка объекта портфеля (getMyProperty)

Назначение

Детальная карточка объекта портфеля инвестора (переход из списка «Мои объекты недвижимости», /profile/my-lots): полный снапшот объекта недвижимости, за сколько купили/продали, сколько и когда заработали, текущая оценка с нереализованным доходом и признак доступности истории изменения цен — сам график фронт запрашивает напрямую у public API invest-analytics по propertyObjectId (история изменения цен объекта — ссылка на invest-analytics, здесь не хранится и не проксируется). Дом полей — ../../domain/investor-property.md. Общие модели и правило деградации оценки — README.md.

Metadata

ПолеЗначение
Сервис/контекстinvestor-owner
API areapublic
Feature groupportfolio
Статусdraft
Документdocs/06-services/investor-owner/api/public/get-my-property.md

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

ПроверкаЗначение
Actor typeUser (инвестор, Bearer JWT)
Auth policy / Permissionsauthenticated; permission investor-owner.portfolio.view-self (../../../identity/domain/permission.md)
Scope (RBAC)self (роль не требуется)
Record-levelтолько собственная запись: предикат InvestorUserId = sub; чужая запись неотличима от несуществующей (единый 404 — анти-enumeration, README.md)
Tenant isolationне применяется — инвестор вне tenant (ADR-0052)
Auditread-sensitive: финансовые данные — только владельцу; audit-запись не создаётся

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/portfolio/properties/{propertyId}
Success status200
Idempotency headerне применяется (safe GET)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
InvestorUserIdidentity claims (sub)GuidДавалидный JWT — 401 до операцииrecord-level ключ
PropertyIdroute {propertyId}GuidДавалидный Guid (иначе 404 route-binding / ValidationError)id записи портфеля, не PropertyObjectId

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

ПолеТипОбязательностьИсточникMaskingОписание
idGuidДаInvestorPropertyEntity.IdId записи портфеля.
propertyObjectIdGuidДаPropertyObjectIdОбъект недвижимости (chessboard); ключ для истории цен и актуальной карточки объекта недвижимости в catalog.
projectIdGuid?НетProjectIdПроект; null — объект не сопоставлен с шахматкой PIN.
buildingIdGuid?НетBuildingIdЗдание/литер.
developerIdGuid?НетDeveloperIdЗастройщик.
statusstring (enum OwnershipStatus)ДаStatusActive / Sold.
sourcestring (enum PropertySource)ДаSourcePinCrmDeal / TalentOwnersSync.
propertyObjectPropertyObjectSnapshotModelДаVO PropertyObjectSnapshot (README.md)Иммутабельный снапшот «что купили».
purchasePriceMoneyModelДаPurchasePriceтолько владельцуЗа сколько купили.
purchasedAtstring (ISO 8601)ДаPurchasedAtКогда купили.
salePriceMoneyModel?Нет (обязателен при status = Sold, INV-3)SalePriceтолько владельцуЗа сколько продали.
soldAtstring (ISO 8601)?Нет (обязателен при status = Sold)SoldAtКогда продали / когда заработали.
realizedProfitMoneyModel?Нет (обязателен при status = Sold)RealizedProfit (зафиксирован при продаже, INV-5)только владельцуСколько заработали (< 0 — убыток).
currentValuationCurrentValuationModelДаinvest-analytics + computed (README.md)только владельцуТекущая оценка и нереализованный доход; available = false для Sold / projectId = null / деградации.
priceHistoryAvailableboolДаcomputed: ProjectId != nullДоступен ли график истории цен: true → фронт вызывает invest-analytics по propertyObjectId (../../../invest-analytics/api/public/get-property-object-price-history.md); false — объект Talent Owners без сопоставления, показывается только снапшот.
createdAtstring (ISO 8601)ДаCreatedAtПоявление записи в портфеле.

OwnerMappingId, DealId, ExternalOwnershipId, audit-поля (*_ByUserId) в ответ не попадают — внутренние ключи идемпотентности и трассировки (доступны Службе заботы через admin-операции).

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (InvestorUserId — из claim sub); требуется permission investor-owner.portfolio.view-self (иначе 403); scope self. Читается только собственная запись (record-level).
  2. Проверка входа и предусловий. PropertyId != Guid.Empty → иначе ValidationError. Найти запись портфеля по паре (Id = PropertyId, InvestorUserId = actor, не удалённая); если нет → INVESTOR_OWNER_PROPERTY_NOT_FOUND — один код для «нет записи», «чужая запись» и «запись архивирована» (анти-enumeration). Карточка читается в любом статусе (Active/Sold).
  3. Что читаем. Запись портфеля (по условию выше). При Status = Active и ProjectId != null — текущая оценка одного объекта в invest-analytics (getLotsCurrentPrices с одним id; timeout 500 мс, без retry; сбой → деградация available = false, README.md); иначе оценка не запрашивается. Порядок: сначала запись (даёт PropertyObjectId/Status/ProjectId и решает, нужна ли оценка).
  4. Маппинг. Поле-в-поле — таблица ответа; priceHistoryAvailable = ProjectId != null; currentValuation — по правилу README.md: unrealizedProfit.amount = currentPrice.amount − PurchasePrice.Amount при совпадении валют, иначе null; realizedProfit не пересчитывается (читается зафиксированным).
  5. Результат. 200; запись не найдена/чужая/архивирована → INVESTOR_OWNER_PROPERTY_NOT_FOUND (404). Консистентность: запись — read-your-writes; currentValuation — eventual (лента цен ≤ 5 мин + допустимая деградация).

Диаграмма

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

ТипДетали
Внешние вызовыinvest-analytics getLotsCurrentPrices (1 объект недвижимости, timeout 500 мс, без retry, best-effort); история цен НЕ проксируется — фронт ходит в invest-analytics сам
Проекции / поискnone (чтение по PK + record-level предикату)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен/невалиден JWTбез тела схемыre-login
ValidationError400PropertyId пустой/не GuidСписок ошибок полейno retry
FORBIDDEN403Нет investor-owner.portfolio.view-self«Недостаточно прав»no retry
INVESTOR_OWNER_PROPERTY_NOT_FOUND404Записи нет / принадлежит другому инвестору / архивирована«Объект портфеля не найден»no retry
— (деградация, не ошибка)200invest-analytics недоступен/timeoutcurrentValuation.available = falseфронт может перезапросить

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

  • Новые optional поля — additive; выведение DealId/OwnerMappingId в ответ потребует security review; breaking-правило — README.md.
  • Logs: InvestorUserId, PropertyId, PropertyObjectId, флаг деградации — без сумм; metrics: investor_owner_my_property_requests_total{result}, latency histogram, investor_owner_current_valuation_degraded_total; traces: сквозной trace + вложенный span invest-analytics; dashboard investor-owner-overview.

Acceptance Criteria

  • Given инвестор и его собственная Active-запись с projectId != null, When GET /portfolio/properties/{propertyId}, Then 200 с полной карточкой: снапшот «что купили», purchasePrice/purchasedAt, currentValuation.available = true (при полученной оценке), priceHistoryAvailable = true; внутренние ключи (DealId, OwnerMappingId, ExternalOwnershipId, audit-поля) в ответ не попадают.
  • Given собственная Sold-запись, When запрос, Then 200 с salePrice/soldAt/realizedProfit (зафиксированы при продаже, не пересчитываются), currentValuation.available = false.
  • Given запись без сопоставления с шахматкой (projectId = null, объект Talent Owners), When запрос, Then 200, priceHistoryAvailable = false, currentValuation.available = false — показывается только снапшот.
  • Given propertyId чужой записи, несуществующей записи или архивированной записи, When запрос, Then единый INVESTOR_OWNER_PROPERTY_NOT_FOUND (404) — три случая неразличимы (анти-enumeration).
  • Given invest-analytics недоступен/таймаут, When запрос активной сопоставленной записи, Then 200 с currentValuation.available = false (карточка не падает).
  • Given пустой/невалидный propertyId, When запрос, Then ValidationError/route-binding 404; Given нет investor-owner.portfolio.view-self, Then 403; невалидный JWT — 401.

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

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

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