UC-OWN-003. Инвестор просматривает портфель «Мои объекты недвижимости» и доходность
Назначение
Описывает пользовательский сценарий просмотра портфеля: инвестор открывает раздел «Мои объекты
недвижимости» (/profile/my-lots) и видит список своих объектов и сводку доходности — за сколько купил и
продал, сколько и когда заработал. Данные читаются только по владельцу (record-level по InvestorUserId);
операция ничего не меняет. Связан с контекстом investor-owner.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-OWN-003 |
| Контекст | investor-owner (см. docs/06-services/investor-owner/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор (авторизованный UserEntity.Type = Investor) |
| Permissions / scope | investor-owner.portfolio.view-self (self-scope); Служба заботы / Менеджер ПИН читают чужой портфель отдельным admin-API под investor-owner.portfolio.view (платформенный scope, с аудитом доступа) — не этот UC |
| Tenant | TenantId = null — инвестор вне tenant (ADR-0052) |
| Record-level | обязательный фильтр InvestorUserId = actor.UserId (record-level изоляция; тесты изоляции обязательны) |
Предусловие
- Инвестор авторизован (access-JWT identity),
Type = Investor. - В портфеле есть записи
InvestorPropertyEntity(из UC-OWN-001 или UC-OWN-002); пустой портфель — валидный кейс (A1). - Сервис
investor-ownerдоступен;invest-analytics/catalog— опциональные зависимости текущей оценки (graceful degradation — A2).
Основной Поток
- Инвестор открывает раздел «Мои объекты недвижимости»; интерфейс параллельно запрашивает список и сводку.
- Интерфейс вызывает
GET /api/v1/investor-owner/my/properties?status=Active&page=1&pageSize=20(api-метод ../api/public/get-my-properties.md, будущий путь) иGET /api/v1/investor-owner/my/portfolio-summary(../api/public/get-my-portfolio-summary.md, будущий путь) с access-JWT. - Система проверяет JWT, определяет актора (инвестор,
UserId) и его правоinvestor-owner.portfolio.view-self. - Список «Мои объекты недвижимости»:
- Проверка входа:
Page >= 1,PageSize ∈ [1..100],Status— валидное значениеOwnershipStatusилиnull(все статусы); при нарушении —ValidationError. - Читаем страницу записей портфеля владельца (record-level фильтр
InvestorUserId = actor.UserId, только не удалённые; при заданномStatus— с фильтром по статусу), сортировкаPurchasedAtубыв. с детерминированным tiebreak поId(стабильная пагинация при равных датах покупки). Страница за пределами диапазона (Pageбольше числа страниц) → пустойItemsпри корректномTotalCount, HTTP 200. - Маппинг элемента списка
InvestorPropertyListItemModel(дельта от дома полей ../domain/investor-property.md):Id;PropertyObjectId;ProjectId;Title ← PropertyObjectSnapshot.Title;Number ← PropertyObjectSnapshot.Number;ProjectName ← PropertyObjectSnapshot.Project?.ProjectName;AreaTotal ← PropertyObjectSnapshot.AreaTotal;Floor ← PropertyObjectSnapshot.Floor;Status;Source;PurchasePrice {Amount, CurrencyId};PurchasedAt;SalePrice;SoldAt;RealizedProfit(дляSold);HasPriceHistory ← (ProjectId != null)(объект сопоставлен с шахматкой PIN — история цен доступна, см. UC-OWN-004 A2). - Результат: контейнер
{ Items: InvestorPropertyListItemModel[], Page, PageSize, TotalCount }; суммы отдаются только владельцу (record-level гарантирован) и не пишутся в логи.
- Проверка входа:
- Параллельно — сводка портфеля
PortfolioSummaryModel:- Читаем агрегат по записям владельца (
InvestorUserId = actor.UserId, только не удалённые; портфель — единицы-десятки строк, без кэшей и материализаций, ADR-0056). - Расчёт:
TotalInvested=Σ PurchasePrice.Amountпо всем записям (валюта по умолчанию —RUB; при разных валютах суммы группируются поCurrencyIdвTotalsByCurrency[]— ключCurrencyId+ displayCurrencyIsoCode, см. ../api/public/get-my-portfolio-summary.md);TotalEarned=Σ RealizedProfit.Amountпо записямStatus = Sold(«сколько заработали»);LastEarnedAt=max(SoldAt)поSold(«когда заработали»;null, если продаж нет);ActiveCount= число записейStatus = Active;SoldCount= числоStatus = Sold;TotalSalesAmount=Σ SalePrice.AmountпоSold(«за сколько продали», сводно).
- Результат:
PortfolioSummaryModel.
- Читаем агрегат по записям владельца (
- Опционально (текущая оценка активных объектов): интерфейс батч-запросом получает текущие цены объектов
недвижимости из
invest-analyticsпо спискуPropertyObjectIdактивных записей (будущий путь ../../invest-analytics/api/get-lot-current-prices.md); нереализованный доходUnrealizedProfit = CurrentPrice − PurchasePrice.Amountвычисляется на чтение и НЕ хранится вinvestor-owner(README «НЕ делает»). - Интерфейс рендерит: табы Active/Sold, карточки с ценой покупки/продажи и доходом, сводку («всего
вложено / всего заработано / активных / продано»); переключение таба → повторный запрос шага 2 с другим
status. - Клик по объекту открывает карточку — UC-OWN-004.
Альтернативные Потоки
A1. Пустой портфель
- Выборка возвращает
TotalCount = 0; сводка — нули (TotalInvested = 0,TotalEarned = 0). - Интерфейс показывает empty-state «У вас пока нет объектов». Сценарий завершается успешно.
A2. invest-analytics/catalog недоступны (текущая оценка)
- Шаг 6 завершается ошибкой/таймаутом → интерфейс показывает портфель без текущей оценки и без
UnrealizedProfit(graceful degradation, README «Зависимости»); цены покупки/продажи иRealizedProfit— изinvestor-owner, не затронуты. - Возврат к основному потоку при следующем обновлении страницы.
A3. Фильтр по проданным («сколько и когда заработали»)
- Инвестор переключает таб «Проданные» → запрос
status=Sold. - Список показывает по каждой записи
SalePrice,SoldAt,RealizedProfit(зафиксированы при продаже, INV-3 SoldCompleteness); сортировка —SoldAt desc(параметрorderBy=soldAt).
A4. Пользователь не инвестор
- Авторизованный пользователь с
Type != Investor(агент/менеджер) вызывает API → отсутствует правоinvestor-owner.portfolio.view-self→FORBIDDEN→ HTTP 403. - Интерфейс не показывает раздел «Мои объекты недвижимости» таким ролям (защита и на UI, и на API).
Постусловие
- Состояние системы не изменилось (read-only: ни мутаций, ни доменных, ни интеграционных событий).
- Инвестор увидел только собственные записи (record-level
InvestorUserId = actor); чужие записи недоступны ни при каких параметрах запроса. - Показатели консистентны на момент чтения одной БД-выборки (без кэшей — свежесть равна PostgreSQL).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
UNAUTHORIZED | 401 | Отсутствует/просрочен access-JWT | refresh-flow identity; повтор запроса |
FORBIDDEN | 403 | Нет permission investor-owner.portfolio.view-self (A4) | скрыть раздел, показать «недоступно» |
ValidationError | 400 | Page < 1, PageSize > 100, неизвестный status | исправить параметры запроса |
INTERNAL_ERROR | 500 | Инфраструктурный сбой (PostgreSQL down) | retry с backoff; error-state страницы |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Инвестор видит только свои записи; запрос под другим инвестором не возвращает чужие | тест изоляции record-level (обязателен, README «Безопасность») |
| AC2 | Список содержит цену покупки/дату, для Sold — цену продажи, дату и RealizedProfit | контракт-тест ответа |
| AC3 | Сводка: TotalEarned = Σ RealizedProfit (Sold), TotalInvested = Σ PurchasePrice, счётчики верны | тест агрегата на фикстуре из 3 Active + 2 Sold |
| AC4 | Фильтр status и пагинация работают, сортировка PurchasedAt desc по умолчанию | тест выборки |
| AC5 | Пустой портфель → TotalCount = 0, нулевая сводка, HTTP 200 (A1) | тест пустого состояния |
| AC6 | Недоступность invest-analytics не ломает страницу — портфель отдаётся без текущей оценки (A2) | тест деградации |
| AC7 | Не-инвестор получает 403 (A4) | тест авторизации |
| AC8 | Суммы не пишутся в логи открытым текстом | проверка логов |
Наблюдаемость и НФТ
- Латентность: p99 API портфеля и сводки < 300 мс (README «Runtime И Эксплуатация»); нагрузка — единицы-десятки записей на инвестора, без кэшей и материализаций (ADR-0056).
- Логирование операции — без финансовых сумм и PII открытым текстом (AC8): фиксируется факт доступа актора
к своему портфелю (актор,
TotalCount, статус-фильтр), но не значения цен/дохода. - Обязательны тесты record-level изоляции (AC1): выборка под другим инвестором не возвращает чужие записи
ни при каких значениях
Status/Page/PageSize.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (3): UC-OWN-001. Синхронизация собственника из Talent Owners: owner → investor по телефону (system), UC-OWN-002. Завершение сделки в CRM → купленный объект в «Мои объекты недвижимости» (system), UC-OWN-004. Карточка объекта портфеля с историей изменения цен
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы (будущие пути): ../api/public/get-my-properties.md, ../api/public/get-my-portfolio-summary.md.
- Domain entity: ../domain/investor-property.md (дом полей; статусы, INV-3).
- Permissions: ../../identity/domain/permission.md
(
investor-owner.portfolio.view-self). - Текущие цены: ../../invest-analytics/api/get-lot-current-prices.md (будущий путь).
- Связанные UC: UC-OWN-001, UC-OWN-002 (как объекты попадают в портфель), UC-OWN-004 (карточка).
- Решения: ADR-0052 (инвестор вне tenant), ADR-0056 (PostgreSQL-only, без кэшей),
ТехЗадание.md.