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 area | public |
| Feature group | portfolio |
| Статус | draft |
| Документ | docs/06-services/investor-owner/api/public/get-my-property.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (инвестор, Bearer JWT) |
| Auth policy / Permissions | authenticated; 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) |
| Audit | read-sensitive: финансовые данные — только владельцу; audit-запись не создаётся |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/v1/portfolio/properties/{propertyId} |
| Success status | 200 |
| Idempotency header | не применяется (safe GET) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
InvestorUserId | identity claims (sub) | Guid | Да | валидный JWT — 401 до операции | record-level ключ |
PropertyId | route {propertyId} | Guid | Да | валидный Guid (иначе 404 route-binding / ValidationError) | id записи портфеля, не PropertyObjectId |
Модель ответа GetMyPropertyResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
id | Guid | Да | InvestorPropertyEntity.Id | — | Id записи портфеля. |
propertyObjectId | Guid | Да | PropertyObjectId | — | Объект недвижимости (chessboard); ключ для истории цен и актуальной карточки объекта недвижимости в catalog. |
projectId | Guid? | Нет | ProjectId | — | Проект; null — объект не сопоставлен с шахматкой PIN. |
buildingId | Guid? | Нет | BuildingId | — | Здание/литер. |
developerId | Guid? | Нет | DeveloperId | — | Застройщик. |
status | string (enum OwnershipStatus) | Да | Status | — | Active / Sold. |
source | string (enum PropertySource) | Да | Source | — | PinCrmDeal / TalentOwnersSync. |
propertyObject | PropertyObjectSnapshotModel | Да | VO PropertyObjectSnapshot (README.md) | — | Иммутабельный снапшот «что купили». |
purchasePrice | MoneyModel | Да | PurchasePrice | только владельцу | За сколько купили. |
purchasedAt | string (ISO 8601) | Да | PurchasedAt | — | Когда купили. |
salePrice | MoneyModel? | Нет (обязателен при status = Sold, INV-3) | SalePrice | только владельцу | За сколько продали. |
soldAt | string (ISO 8601)? | Нет (обязателен при status = Sold) | SoldAt | — | Когда продали / когда заработали. |
realizedProfit | MoneyModel? | Нет (обязателен при status = Sold) | RealizedProfit (зафиксирован при продаже, INV-5) | только владельцу | Сколько заработали (< 0 — убыток). |
currentValuation | CurrentValuationModel | Да | invest-analytics + computed (README.md) | только владельцу | Текущая оценка и нереализованный доход; available = false для Sold / projectId = null / деградации. |
priceHistoryAvailable | bool | Да | computed: ProjectId != null | — | Доступен ли график истории цен: true → фронт вызывает invest-analytics по propertyObjectId (../../../invest-analytics/api/public/get-property-object-price-history.md); false — объект Talent Owners без сопоставления, показывается только снапшот. |
createdAt | string (ISO 8601) | Да | CreatedAt | — | Появление записи в портфеле. |
OwnerMappingId, DealId, ExternalOwnershipId, audit-поля (*_ByUserId) в ответ не попадают —
внутренние ключи идемпотентности и трассировки (доступны Службе заботы через admin-операции).
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор (
InvestorUserId— из claimsub); требуется permissioninvestor-owner.portfolio.view-self(иначе403); scope self. Читается только собственная запись (record-level). - Проверка входа и предусловий.
PropertyId != Guid.Empty→ иначеValidationError. Найти запись портфеля по паре (Id = PropertyId,InvestorUserId = actor, не удалённая); если нет →INVESTOR_OWNER_PROPERTY_NOT_FOUND— один код для «нет записи», «чужая запись» и «запись архивирована» (анти-enumeration). Карточка читается в любом статусе (Active/Sold). - Что читаем. Запись портфеля (по условию выше). При
Status = ActiveиProjectId != null— текущая оценка одного объекта вinvest-analytics(getLotsCurrentPricesс одним id; timeout 500 мс, без retry; сбой → деградацияavailable = false, README.md); иначе оценка не запрашивается. Порядок: сначала запись (даётPropertyObjectId/Status/ProjectIdи решает, нужна ли оценка). - Маппинг. Поле-в-поле — таблица ответа;
priceHistoryAvailable = ProjectId != null;currentValuation— по правилу README.md:unrealizedProfit.amount = currentPrice.amount − PurchasePrice.Amountпри совпадении валют, иначеnull;realizedProfitне пересчитывается (читается зафиксированным). - Результат.
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 |
ValidationError | 400 | PropertyId пустой/не Guid | Список ошибок полей | no retry |
FORBIDDEN | 403 | Нет investor-owner.portfolio.view-self | «Недостаточно прав» | no retry |
INVESTOR_OWNER_PROPERTY_NOT_FOUND | 404 | Записи нет / принадлежит другому инвестору / архивирована | «Объект портфеля не найден» | no retry |
| — (деградация, не ошибка) | 200 | invest-analytics недоступен/timeout | currentValuation.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 + вложенный spaninvest-analytics; dashboardinvestor-owner-overview.
Acceptance Criteria
- Given инвестор и его собственная
Active-запись сprojectId != null, WhenGET /portfolio/properties/{propertyId}, Then200с полной карточкой: снапшот «что купили»,purchasePrice/purchasedAt,currentValuation.available = true(при полученной оценке),priceHistoryAvailable = true; внутренние ключи (DealId,OwnerMappingId,ExternalOwnershipId, audit-поля) в ответ не попадают. - Given собственная
Sold-запись, When запрос, Then200сsalePrice/soldAt/realizedProfit(зафиксированы при продаже, не пересчитываются),currentValuation.available = false. - Given запись без сопоставления с шахматкой (
projectId = null, объект Talent Owners), When запрос, Then200,priceHistoryAvailable = false,currentValuation.available = false— показывается только снапшот. - Given
propertyIdчужой записи, несуществующей записи или архивированной записи, When запрос, Then единыйINVESTOR_OWNER_PROPERTY_NOT_FOUND(404) — три случая неразличимы (анти-enumeration). - Given
invest-analyticsнедоступен/таймаут, When запрос активной сопоставленной записи, Then200сcurrentValuation.available = false(карточка не падает). - Given пустой/невалидный
propertyId, When запрос, ThenValidationError/route-binding 404; Given нетinvestor-owner.portfolio.view-self, Then403; невалидный JWT —401.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: InvestorPropertyEntity
- API (3): GET /api/v1/portfolio/properties — Мои объекты (getMyProperties), GET /api/v1/portfolio/summary — Сводка доходности портфеля (getMyPortfolioSummary), Investor-Owner API — реестр операций и общие контрактные модели
- Use Cases (1): UC-OWN-004. Карточка объекта портфеля с историей изменения цен
- registries (1): Реестр API-методов PIN
Связанные документы
- Реестр и общие модели: README.md.
- Сущность: ../../domain/investor-property.md (дом полей, INV-1..9); связь с собственником: ../../domain/owner-mapping.md.
- Список: get-my-properties.md; сводка: get-my-portfolio-summary.md.
- История цен: ../../../invest-analytics/api/public/get-property-object-price-history.md;
текущая оценка:
../../../invest-analytics/api/get-lots-current-prices.md
(assumption); актуальные данные объекта недвижимости —
catalog/chessboardпоpropertyObjectId.