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

UC-OWN-004. Карточка объекта портфеля с историей изменения цен

Назначение

Описывает пользовательский сценарий просмотра карточки объекта портфеля: инвестор открывает объект из «Мои объекты недвижимости» и видит детали покупки/продажи/дохода, актуальные данные объекта недвижимости и историю изменения цен объекта — график из invest-analytics по PropertyObjectId. Данные записи читаются только по владельцу (record-level по InvestorUserId); операция ничего не меняет. Связан с контекстом investor-owner.

Metadata

ПолеЗначение
IDUC-OWN-004
Контекстinvestor-owner (см. docs/06-services/investor-owner/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольИнвестор (авторизованный UserEntity.Type = Investor, владелец записи)
Permissions / scopeinvestor-owner.portfolio.view-self (self-scope); история цен в invest-analytics — публичные/квази-публичные данные объекта недвижимости, отдаются по PropertyObjectId без раскрытия чужих сделок
TenantTenantId = null — инвестор вне tenant (ADR-0052)
Record-levelInvestorUserId = actor.UserId (record-level); чужая запись неотличима от несуществующей (404, masking)

Предусловие

  • Инвестор авторизован; в портфеле есть запись InvestorPropertyEntity c известным Id (переход из списка UC-OWN-003).
  • invest-analytics хранит историю цен по объектам недвижимости PIN (ClickHouse append-only); для объектов Talent Owners, не сопоставленных с шахматкой (ProjectId = null), истории нет — A2.
  • catalog/chessboard доступны для актуальных данных объекта недвижимости (опционально — A3).

Триггер

ПолеЗначение
ИсточникUI button / переход по ссылке
Триггерящий элементКлик по карточке объекта в списке /profile/my-lots → страница/модал карточки объекта портфеля

Основной Поток

  1. Инвестор кликает объект в «Мои объекты недвижимости»; интерфейс переходит на карточку с propertyId из списка.
  2. Интерфейс вызывает GET /api/v1/investor-owner/my/properties/{propertyId} (api-метод ../api/public/get-my-property.md, будущий путь) с access-JWT.
  3. Система проверяет вход и доступ: PropertyId != Guid.Empty (иначе ValidationError); право investor-owner.portfolio.view-self; читать можно в любом статусе (Active/Sold); принадлежность обеспечивается record-level фильтром (шаг 4).
  4. Карточка объекта:
    1. Читаем запись портфеля по Id = propertyId (только не удалённую) с record-level фильтром InvestorUserId = actor.UserId; если записи нет ЛИБО она чужая (неразличимо, masking) → ошибка INVESTOR_OWNER_PROPERTY_NOT_FOUND → HTTP 404. Карточка строится из одной записи (снапшот денормализован), других агрегатов не требуется.
    2. Маппинг 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.
    3. Результат: InvestorPropertyCardModel; финансовые поля отдаются только владельцу, суммы не логируются.
  5. Интерфейс параллельно догружает по 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, будущий путь).
  6. Для Status = Active интерфейс вычисляет нереализованный доход на чтение: UnrealizedProfit = CurrentPrice.Amount − PurchasePrice.Amount (read-проекция; в investor-owner НЕ хранится — README «НЕ делает»); для Status = Sold показывается зафиксированный RealizedProfit и точка продажи на графике (SoldAt, SalePrice).
  7. Интерфейс рендерит карточку: снапшот «что купили», блок цен и дохода, график истории цен с отметками покупки (PurchasedAt, PurchasePrice) и продажи (при наличии), ссылку на актуальную карточку объекта недвижимости в каталоге.

Альтернативные Потоки

A1. Запись не найдена или принадлежит другому инвестору

  1. Запись по Id с record-level фильтром не найдена → ошибка INVESTOR_OWNER_PROPERTY_NOT_FOUND → HTTP 404 (единый ответ для «нет» и «чужая» — существование чужих записей не раскрываем).
  2. Интерфейс показывает «объект не найден» и возвращает в список. Сценарий завершается.

A2. Объект не сопоставлен с шахматкой PIN (история цен недоступна)

  1. Запись из синка Talent Owners с ProjectId = null (README «Известные Ограничения») → PriceHistoryAvailable = false в ответе шага 4.
  2. Интерфейс не вызывает invest-analytics и скрывает график; карточка показывает только данные снапшота и цены покупки/продажи из investor-owner. Сценарий завершается успешно.

A3. invest-analytics или catalog недоступны

  1. Запросы шага 5 завершаются ошибкой/таймаутом → карточка рендерится без графика и/или без актуальных данных объекта недвижимости (graceful degradation, README «Зависимости»: «Карточка без графика цен; портфель работает»); блок покупки/продажи/RealizedProfit — из ответа investor-owner, не затронут.
  2. Возврат к основному потоку при повторной загрузке.

A4. Проданный объект

  1. Status = Sold → карточка показывает полный итог: PurchasePrice/PurchasedAt, SalePrice/SoldAt, RealizedProfit («сколько и когда заработали», INV-3 гарантирует полноту); текущая цена из catalog не запрашивается (объект больше не в собственности), нереализованный доход не считается.
  2. История цен по-прежнему доступна при ProjectId != null — график строится до SoldAt с отметкой продажи; для объекта без сопоставления с шахматкой (ProjectId = null) графика нет (A2).

A5. Просмотр Службой заботы (смежный, не self-API)

  1. Оператор заботы открывает запись через admin-API под investor-owner.portfolio.view (платформенный scope) — отдельный endpoint с обязательным аудитом доступа к финансовым данным; self-endpoint этого UC для платформенных ролей закрыт (право не совпадает). Детали — в admin-разделе API README.

Постусловие

  • Состояние системы не изменилось (read-only; событий нет).
  • Инвестор увидел карточку только своей записи: снапшот «что купили», цены покупки/продажи, доход, историю изменения цен (при ProjectId != null) и актуальные данные объекта недвижимости.
  • Кросс-контекстные данные (график, текущая цена) получены напрямую из их «домов» (invest-analytics, catalog) — investor-owner их не хранит и не проксирует.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
UNAUTHORIZED401Отсутствует/просрочен access-JWTrefresh-flow identity; повтор
FORBIDDEN403Нет permission investor-owner.portfolio.view-self (не-инвестор)скрыть раздел
ValidationError400PropertyId = Guid.Empty / не-GUID в путиисправить запрос
INVESTOR_OWNER_PROPERTY_NOT_FOUND404Записи нет, она soft-deleted или принадлежит другому инвестору (A1, masking)вернуть в список «Мои объекты недвижимости»
INTERNAL_ERROR500Инфраструктурный сбойretry с backoff; error-state

Ошибки invest-analytics/catalog (шаг 5) не транслируются в ошибку карточки — деградация A3.

Acceptance Criteria

#КритерийПроверка
AC1Карточка содержит PropertyObjectSnapshot, PurchasePrice/PurchasedAt; для SoldSalePrice/SoldAt/RealizedProfitконтракт-тест ответа для Active и Sold
AC2Чужая запись → 404, неотличимый от несуществующей (A1)тест изоляции record-level
AC3ProjectId != nullPriceHistoryAvailable = true и фронт строит график по PropertyObjectId из invest-analyticse2e-тест с историей цен
AC4Объект Talent без сопоставления → карточка без графика, только снапшот (A2)тест с ProjectId = null
AC5Недоступность invest-analytics/catalog не ломает карточку (A3)тест деградации
AC6UnrealizedProfit не хранится в БД investor-owner — только вычисляется на чтениепроверка схемы + контракта
AC7Soft-deleted запись → 404тест архивной записи
AC8Финансовые суммы не пишутся в логипроверка логов

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

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

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