UC-OWN-004. Карточка объекта портфеля с историей изменения цен
Назначение
Описывает пользовательский сценарий просмотра карточки объекта портфеля: инвестор открывает объект из
«Мои объекты недвижимости» и видит детали покупки/продажи/дохода, актуальные данные объекта недвижимости и
историю изменения цен объекта — график из invest-analytics по PropertyObjectId. Данные записи
читаются только по владельцу (record-level по InvestorUserId); операция ничего не меняет. Связан с
контекстом investor-owner.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-OWN-004 |
| Контекст | investor-owner (см. docs/06-services/investor-owner/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор (авторизованный UserEntity.Type = Investor, владелец записи) |
| Permissions / scope | investor-owner.portfolio.view-self (self-scope); история цен в invest-analytics — публичные/квази-публичные данные объекта недвижимости, отдаются по PropertyObjectId без раскрытия чужих сделок |
| Tenant | TenantId = null — инвестор вне tenant (ADR-0052) |
| Record-level | InvestorUserId = actor.UserId (record-level); чужая запись неотличима от несуществующей (404, masking) |
Предусловие
- Инвестор авторизован; в портфеле есть запись
InvestorPropertyEntityc известнымId(переход из списка UC-OWN-003). invest-analyticsхранит историю цен по объектам недвижимости PIN (ClickHouse append-only); для объектов Talent Owners, не сопоставленных с шахматкой (ProjectId = null), истории нет — A2.catalog/chessboardдоступны для актуальных данных объекта недвижимости (опционально — A3).
Триггер
| Поле | Значение |
|---|---|
| Источник | UI button / переход по ссылке |
| Триггерящий элемент | Клик по карточке объекта в списке /profile/my-lots → страница/модал карточки объекта портфеля |
Основной Поток
- Инвестор кликает объект в «Мои объекты недвижимости»; интерфейс переходит на карточку с
propertyIdиз списка. - Интерфейс вызывает
GET /api/v1/investor-owner/my/properties/{propertyId}(api-метод ../api/public/get-my-property.md, будущий путь) с access-JWT. - Система проверяет вход и доступ:
PropertyId != Guid.Empty(иначеValidationError); правоinvestor-owner.portfolio.view-self; читать можно в любом статусе (Active/Sold); принадлежность обеспечивается record-level фильтром (шаг 4). - Карточка объекта:
- Читаем запись портфеля по
Id = propertyId(только не удалённую) с record-level фильтромInvestorUserId = actor.UserId; если записи нет ЛИБО она чужая (неразличимо, masking) → ошибкаINVESTOR_OWNER_PROPERTY_NOT_FOUND→ HTTP 404. Карточка строится из одной записи (снапшот денормализован), других агрегатов не требуется. - Маппинг
InvestorPropertyCardModel(дельта от дома полей ../domain/investor-property.md):- идентификация:
Id,PropertyObjectId,ProjectId,BuildingId,DeveloperId,Source,Status; - «что купили» (иммутабельный снапшот):
PropertyObjectSnapshot {Title, Number, Project {ProjectId, ProjectName}, Building {BuildingId, BuildingName}, AreaTotal, Floor, SnapshotAt}; - «за сколько купили»:
PurchasePrice {Amount, CurrencyId},PurchasedAt; - «за сколько продали / сколько и когда заработали» (только
Status = Sold, INV-3):SalePrice,SoldAt,RealizedProfit(EarnedAt = SoldAt); - ссылки для интерфейса:
PriceHistoryAvailable ← (ProjectId != null);PriceHistoryRef ← { PropertyObjectId }— указатель на API истории ценinvest-analytics(сам графикinvestor-ownerне проксирует — история цен живёт вinvest-analytics);CurrentLotRef ← { PropertyObjectId }— указатель на актуальную карточку объекта недвижимости вcatalog.
- идентификация:
- Результат:
InvestorPropertyCardModel; финансовые поля отдаются только владельцу, суммы не логируются.
- Читаем запись портфеля по
- Интерфейс параллельно догружает по
PropertyObjectId:- историю изменения цен —
invest-analytics get-lot-price-history(propertyObjectId, from, to)(../../invest-analytics/api/public/get-property-object-price-history.md, будущий путь): временной ряд точек{Date, Price {Amount, Currency}}для графика. Окно по умолчанию:from = PurchasedAt(или первая доступная точка ряда, что раньше),to = SoldAtдля проданного объекта либоnowдля активного; на график наносятся отметки покупки (PurchasedAt,PurchasePrice) и, приStatus = Sold, продажи (SoldAt,SalePrice); - актуальные данные объекта недвижимости (текущая цена, статус, фото) — из
catalogпоPropertyObjectId(../../catalog/api/public/get-property-object.md, будущий путь).
- историю изменения цен —
- Для
Status = Activeинтерфейс вычисляет нереализованный доход на чтение:UnrealizedProfit = CurrentPrice.Amount − PurchasePrice.Amount(read-проекция; вinvestor-ownerНЕ хранится — README «НЕ делает»); дляStatus = Soldпоказывается зафиксированныйRealizedProfitи точка продажи на графике (SoldAt,SalePrice). - Интерфейс рендерит карточку: снапшот «что купили», блок цен и дохода, график истории цен с отметками
покупки (
PurchasedAt,PurchasePrice) и продажи (при наличии), ссылку на актуальную карточку объекта недвижимости в каталоге.
Альтернативные Потоки
A1. Запись не найдена или принадлежит другому инвестору
- Запись по
Idс record-level фильтром не найдена → ошибкаINVESTOR_OWNER_PROPERTY_NOT_FOUND→ HTTP 404 (единый ответ для «нет» и «чужая» — существование чужих записей не раскрываем). - Интерфейс показывает «объект не найден» и возвращает в список. Сценарий завершается.
A2. Объект не сопоставлен с шахматкой PIN (история цен недоступна)
- Запись из синка Talent Owners с
ProjectId = null(README «Известные Ограничения») →PriceHistoryAvailable = falseв ответе шага 4. - Интерфейс не вызывает
invest-analyticsи скрывает график; карточка показывает только данные снапшота и цены покупки/продажи изinvestor-owner. Сценарий завершается успешно.
A3. invest-analytics или catalog недоступны
- Запросы шага 5 завершаются ошибкой/таймаутом → карточка рендерится без графика и/или без актуальных
данных объекта недвижимости (graceful degradation, README «Зависимости»: «Карточка без графика цен;
портфель работает»); блок покупки/продажи/
RealizedProfit— из ответаinvestor-owner, не затронут. - Возврат к основному потоку при повторной загрузке.
A4. Проданный объект
Status = Sold→ карточка показывает полный итог:PurchasePrice/PurchasedAt,SalePrice/SoldAt,RealizedProfit(«сколько и когда заработали», INV-3 гарантирует полноту); текущая цена изcatalogне запрашивается (объект больше не в собственности), нереализованный доход не считается.- История цен по-прежнему доступна при
ProjectId != null— график строится доSoldAtс отметкой продажи; для объекта без сопоставления с шахматкой (ProjectId = null) графика нет (A2).
A5. Просмотр Службой заботы (смежный, не self-API)
- Оператор заботы открывает запись через admin-API под
investor-owner.portfolio.view(платформенный scope) — отдельный endpoint с обязательным аудитом доступа к финансовым данным; self-endpoint этого UC для платформенных ролей закрыт (право не совпадает). Детали — в admin-разделе API README.
Постусловие
- Состояние системы не изменилось (read-only; событий нет).
- Инвестор увидел карточку только своей записи: снапшот «что купили», цены покупки/продажи, доход, историю
изменения цен (при
ProjectId != null) и актуальные данные объекта недвижимости. - Кросс-контекстные данные (график, текущая цена) получены напрямую из их «домов» (
invest-analytics,catalog) —investor-ownerих не хранит и не проксирует.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
UNAUTHORIZED | 401 | Отсутствует/просрочен access-JWT | refresh-flow identity; повтор |
FORBIDDEN | 403 | Нет permission investor-owner.portfolio.view-self (не-инвестор) | скрыть раздел |
ValidationError | 400 | PropertyId = Guid.Empty / не-GUID в пути | исправить запрос |
INVESTOR_OWNER_PROPERTY_NOT_FOUND | 404 | Записи нет, она soft-deleted или принадлежит другому инвестору (A1, masking) | вернуть в список «Мои объекты недвижимости» |
INTERNAL_ERROR | 500 | Инфраструктурный сбой | retry с backoff; error-state |
Ошибки invest-analytics/catalog (шаг 5) не транслируются в ошибку карточки — деградация A3.
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Карточка содержит PropertyObjectSnapshot, PurchasePrice/PurchasedAt; для Sold — SalePrice/SoldAt/RealizedProfit | контракт-тест ответа для Active и Sold |
| AC2 | Чужая запись → 404, неотличимый от несуществующей (A1) | тест изоляции record-level |
| AC3 | ProjectId != null → PriceHistoryAvailable = true и фронт строит график по PropertyObjectId из invest-analytics | e2e-тест с историей цен |
| AC4 | Объект Talent без сопоставления → карточка без графика, только снапшот (A2) | тест с ProjectId = null |
| AC5 | Недоступность invest-analytics/catalog не ломает карточку (A3) | тест деградации |
| AC6 | UnrealizedProfit не хранится в БД investor-owner — только вычисляется на чтение | проверка схемы + контракта |
| AC7 | Soft-deleted запись → 404 | тест архивной записи |
| AC8 | Финансовые суммы не пишутся в логи | проверка логов |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-OWN-002. Завершение сделки в CRM → купленный объект в «Мои объекты недвижимости» (system), UC-OWN-003. Инвестор просматривает портфель «Мои объекты недвижимости» и доходность
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API метод: ../api/public/get-my-property.md (будущий путь).
- Domain entity: ../domain/investor-property.md (дом полей:
PropertyObjectSnapshot,Money, INV-3/9). - История цен: ../../invest-analytics/api/public/get-property-object-price-history.md (будущий путь).
- Актуальный объект недвижимости: ../../catalog/api/public/get-property-object.md (будущий путь); структура объекта недвижимости — ../../chessboard/domain/property-object.md (будущий путь).
- Permissions: ../../identity/domain/permission.md.
- Связанные UC: UC-OWN-003 (список-источник перехода),
UC-OWN-001 / UC-OWN-002
(происхождение записи и
Source). - Решения: ADR-0052, ADR-0056, ADR-0057/
invest-analytics(история цен — ClickHouse),ТехЗадание.md.