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

UC-CTL-008. Карточка объекта (полная: аналитика, калькулятор, PDF, избранное, бронь)

Назначение

Авторизованный пользователь открывает полную карточку объекта недвижимости: все характеристики (площадь, этаж, комнатность, цена, статус), инвестиционная аналитика (график роста цены, прогноз доходности, ROI, окупаемость), калькулятор, PDF-отчёт, добавление в избранное/подборку, бронирование. Карточку и комиссию агента отдаёт каталог (CatalogPropertyObjectCard); аналитику/калькулятор/PDF — invest-analytics, избранное — favorites, подборки — selections, бронирование — crm; действия записываются в user-activity.

Metadata

ПолеЗначение
IDUC-CTL-008
Контекстcatalog (см. docs/06-services/catalog/) + чтение invest-analytics; переходы в favorites/selections/crm
Типосновной
Статусdraft

Актор

ПолеЗначение
РольИнвестор / Агент / МенеджерПИН (авторизованные)
Permissions / scopeвалидный JWT; агент дополнительно — catalog.commissions.view для блока комиссии
Tenantне применяется — платформенная витрина (ADR-0052 п.3); комиссия — internal-класс данных, выдаётся по permission
Record-levelне применяется

Предусловие

  • Пользователь авторизован (JWT валиден).
  • CatalogPropertyObjectCard.Status = Published; метрики invest-analytics синхронизированы (AnalyticsSyncedAt заполнен; при отсутствии — деградация A4).

Триггер

ПолеЗначение
ИсточникUI navigation
Триггерящий элементОткрытие /lots/[id]/info (+вкладка /lots/[id]/analytics) авторизованным пользователем

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

  1. Пользователь открывает полную карточку объекта; интерфейс проверяет JWT (нет — UC-CTL-007 A1).
  2. Интерфейс запрашивает карточку объекта по id — та же операция, что UC-CTL-007, но для авторизованного актора без отсечения полей:
    • Проверка входа/загрузка. id — Guid, Status = Published (иначе 404 CATALOG_PROPERTY_OBJECT_NOT_FOUND).
    • Состав — полный ApartmentDetail (все характеристики): areaTotal, areaLiving, floor, floorsTotal, roomsCount, ceilingHeight, price, pricePerMeter, propertyObjectStatus (Free/Reserved/Booked/Sold — «свободно/забронировано»), deliveryDate, readinessPercent, payback, reliability, growthPercent, rentalIncomePerYear, rating, visualizationFileId; analyticsAvailable = true.
    • Блок комиссии. При permission catalog.commissions.view в ответ добавляется agentCommission { percentMin, percentMax, baseKind, rewardRuleId } — комиссия агента на странице объекта (VO PropertyObjectAgentCommission, денормализована консюмером pin.rewards.commission-rules.v1).
    • Результат — карточка.
  3. Интерфейс открывает вкладку аналитики /lots/[id]/analytics и запрашивает invest-analytics (его API — docs/06-services/invest-analytics/): график роста цены по истории цен (ClickHouse), прогноз доходности/ROI/окупаемость, серии InvestorMonth с параметрами юр/физлицо + УСН/ОСНО.
  4. Пользователь использует калькулятор ипотеки и доходности: ввод параметров → серверный пересчёт в invest-analytics в реальном времени; программы покупки/график платежей — crm (booking).
  5. Пользователь скачивает PDF-отчёт по объекту: интерфейс вызывает invest-analytics (формирование отчёта и отдача файла).
  6. Пользователь добавляет объект в избранное или подборку: вызовы favorites / selections по propertyObjectId (карточки живут по id и переживают делистинг — Delisted-retention 30 дней). Дома сценариев: docs/06-services/favorites/, docs/06-services/selections/.
  7. Пользователь нажимает «Забронировать» (доступно при propertyObjectStatus = Free): форма ФИО+телефон уходит в crm (booking/leads; каталог заявку не обрабатывает). Смена статуса вернётся в карточку цепочкой crm → chessboard → pin.chessboard.property-objects.v1 → catalog.
  8. Запись действий в аудит: интерфейс шлёт в user-activity открытие PropertyObjectView (+heartbeat, как UC-CTL-007) и типизированные ActionEvent-ы действий (CalculatorUsed, PdfDownloaded, AddedToFavorites, BookingRequested) — пробрасываются в карточку лида агента (crm) и статистику застройщика (dev-stats).

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

A1. Пользователь не авторизован

  1. Переход к UC-CTL-007 (ограниченная карточка с CTA входа); после входа — возврат к шагу 2.

A2. Актор — агент (комиссия)

  1. Шаг 2 включает agentCommission в ответ (диапазон percentMin..percentMax, когда матчится несколько правил застройщика — тип/планировка/группа/комнатность/площадь).
  2. Если комиссия для объекта недвижимости не задана (AgentCommission = null), блок не отображается (правило VO — catalog-property-object-card.md). Поток не меняется.

A3. Объект недвижимости недоступен для брони (Reserved/Booked/Sold)

  1. Кнопка «Забронировать» неактивна; отображается статус и, для Reserved, таймер (дом переходов статуса — chessboard; глоссарий «Статус объекта»). Шаг 7 пропускается.

A4. Метрики аналитики ещё не синхронизированы

  1. Payback/GrowthPercent/RentalIncomePerYear = null (AnalyticsSyncedAt пуст) — карточка отображается без блока метрик; вкладка аналитики запрашивает invest-analytics напрямую и может отдать данные раньше витрины (лаг консюмера метрик < 15 мин — README «SLO»). Поток не меняется.

A5. Конкурентная смена статуса во время просмотра

  1. Пользователь смотрит карточку, объект недвижимости уходит в Reserved (другой инвестор). Каталог отдаёт реплику на момент запроса; попытка брони по устаревшему Free отклоняется crm проверкой в источнике — каталог ошибок не генерирует. Интерфейс обновляет статус по ответу crm.

A6. Частичный отказ зависимости блока (аналитика/избранное/подборки/бронь)

  1. Ядро карточки (шаг 2, каталог) уже отдано. Отказ одного из соседних контекстов деградирует только свой блок, карточка остаётся:
    • invest-analytics недоступен/таймаут → вкладка аналитики и калькулятор показывают skeleton + повтор; агрегатные метрики карточки (если синхронизированы) остаются, т.к. денормализованы в витрине;
    • favorites/selections недоступны → действие «В избранное/подборку» возвращает ошибку своего контекста, тост об ошибке, карточка активна;
    • crm недоступен → форма брони показывает ошибку и предлагает повтор; статус объекта не меняется.
  2. Пользователь может продолжать работать с остальной карточкой. Сценарий завершается частично успешно.

A7. Повторные действия (идемпотентность)

  1. Повторное «В избранное» по тому же propertyObjectId не создаёт дубликат — favorites идемпотентен по паре (пользователь, объект); интерфейс отражает уже добавленное состояние.
  2. Повторная заявка «Забронировать» по тому же объекту дедуплицируется crm (существующие лид/бронь); пользователь видит уже созданную заявку, второго лида не появляется.
  3. Повторная доставка ActionEvent не задваивает запись в user-activity (дедуп по ключу события) и не искажает карточку лида/статистику.

Инварианты, статусы и валидации

Правила полной карточки (read-проекция каталога; мутации — в соседних контекстах; дом полей — catalog-property-object-card.md):

  • Валидация входа. id — непустой Guid, иначе ValidationError (400). JWT валиден; отсутствие/ просрочка → Unauthorized (401) → A1 (ограниченная карточка / вход).
  • INV-1 VisibilityRule. Отдаётся только Status = Published; Hidden/Delisted/несуществующий → одинаковый CATALOG_PROPERTY_OBJECT_NOT_FOUND (404).
  • INV-3 EffectiveFields. Все display-значения — эффективные (Override(поле) ?? SourceSnapshot(поле)).
  • INV-5 CommissionConsistency. agentCommission включается только при permission catalog.commissions.view (иначе поле отсутствует в JSON, не null); диапазон percentMin..percentMax появляется, когда матчатся несколько правил застройщика (тип/планировка/группа/комнатность/площадь); AgentCommission = null → блок комиссии отсутствует даже для агента.
  • Доступность брони. Кнопка «Забронировать» активна только при propertyObjectStatus = Free; Reserved/Booked/Sold → неактивна (A3), для Reserved показывается таймер; форма брони (ФИО + телефон) валидируется и обрабатывается crm, каталог заявку не хранит.
  • Деградация метрик. Пусто AnalyticsSyncedAtPayback/GrowthPercent/RentalIncomePerYear = null, блок метрик карточки скрыт (A4); вкладка аналитики может отдать данные раньше витрины (запрос напрямую в invest-analytics).
  • Статус доступности. Реплика PropertyObjectAvailabilityStatus (Free/Reserved/Booked/Sold/Hidden); дом статуса и переходов — chessboard.

Кросс-сервисные контракты и согласованность

Карточку и комиссию отдаёт каталог; остальное — соседние контексты (синхронные вызовы клиента) плюс событийные цепочки обратной связи.

  • Что каталог предоставляет. Полный ApartmentDetail из одной строки витрины (+agentCommission при permission). Что каталог ожидает от реплик: chessboard (структура/статус, pin.chessboard.property-objects.v1, ключ PropertyObject.Id, guard INV-4 по Version), invest-analytics (метрики, pin.invest-analytics.property-object-metrics.v1, AnalyticsSyncedAt), rewards (комиссия, pin.rewards.commission-rules.v1, guard INV-5 по RuleVersion). Устаревание: статус — лаг < 1 мин, метрики/комиссия — до ~15 мин; более старое событие пропускается version-guard-ом (проигравший эффекта не оказывает).
  • Аналитика/калькулятор/PDF (синхронно, invest-analytics). Клиент ходит в invest-analytics напрямую: график роста по истории цен, серии InvestorMonth (юр/физлицо + УСН/ОСНО), серверный пересчёт калькулятора в реальном времени, формирование PDF-отчёта. Каталог в этих вызовах не участвует.
  • Избранное/подборки (синхронно, favorites/selections). Запись по propertyObjectId; идемпотентно по паре (пользователь, объект); карточки живут по id и переживают делистинг (Delisted-retention 30 дней), поэтому ссылки не рвутся.
  • Бронирование (синхронно + событийная обратная связь). Порядок: клиент → crm (заявка {ФИО, телефон, propertyObjectId}) → crm проверяет статус на источнике и создаёт бронь/лид → crmchessboard (смена статуса) → chessboard публикует pin.chessboard.property-objects.v1 → консюмер каталога обновляет реплику PropertyObjectStatus (async, лаг < 1 мин) → пользователь видит новый статус при следующем чтении/refresh. Окно eventual-consistency: до применения события карточка ещё показывает прежний статус; попытка брони по устаревшему Free (объект уже Reserved) отклоняется crm — каталог ошибок не генерирует (A5).
  • Аудит действий (async). Клиент → user-activity (типизированные ActionEvent: CalculatorUsed, PdfDownloaded, AddedToFavorites, BookingRequested) → pin.user-activity.actions.v1crm (карточка лида агента), dev-stats (статистика застройщика). Просмотр (PropertyObjectView) и его завершение — цепочка pin.user-activity.property-object-view-ended.v1PopularityViews каталога и dev-stats, как в UC-CTL-007.

Постусловие

  • Пользователь видит полную карточку с аналитикой; выполненные действия (калькулятор, PDF, избранное, заявка на бронь) обработаны целевыми контекстами.
  • В user-activityPropertyObjectView и ActionEvent-ы всех действий; после закрытия просмотра property-object-view-ended инкрементирует PopularityViews карточки.
  • Состояние каталога не изменилось (мутации — в favorites/selections/crm/user-activity).

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

КодHTTPУсловиеПоведение клиента
ValidationError400id — не GuidСтраница ошибки
Unauthorized401JWT отсутствует/истёкA1: ограниченная карточка / вход
CATALOG_PROPERTY_OBJECT_NOT_FOUND404Карточки нет или Status != PublishedСтраница «Объект недоступен»
Ошибки invest-analytics/crm/favoritesпо контрактам этих контекстовдеградация блока, карточка остаётся

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
PropertyObjectViewEnded (производит user-activity)pin.user-activity.property-object-view-ended.v1закрытие просмотра polling-ом (ADR-0056 п.3)catalog (популярность), dev-stats
ActionEventRecorded (производит user-activity)pin.user-activity.actions.v1приём действий шага 8crm (карточка лида)
PropertyObjectUpserted/StatusChanged (производит chessboard)pin.chessboard.property-objects.v1смена статуса после брони (поток crm → chessboard → catalog)catalog (реплика PropertyObjectStatus)

Наблюдаемость и НФТ

  • Логи (без PII): property-object-card-served { cardId, variant=full, commissionIncluded }, booking-handoff { cardId } (ФИО/телефон не логируются каталогом — они уходят в crm), card-not-found { id }. Токены/секреты в логи не пишутся.
  • Метрики: catalog_property_object_card_requests_total{variant="full",result}, catalog_commission_included_total, счётчики деградации по зависимостям (invest-analytics/favorites/selections/crm), доля активной кнопки брони по статусам.
  • НФТ: ядро карточки — чтение одной строки витрины; аналитика/калькулятор/PDF обслуживаются invest-analytics (отдельный бюджет латентности); целевая латентность p95 карточки — единицы десятков мс на стороне каталога (нагрузка — до 10k одновременных, ADR-0056).
  • Безопасность: JWT обязателен; agentCommission — internal-класс данных, только по permission catalog.commissions.view; PII заявки (ФИО/телефон) обрабатывается crm, каталогом не хранится и не логируется.

Acceptance Criteria

#Given (контекст)When (действие)Then (результат)Проверка
AC1Авторизованный актор, опубликованный объектЗапрошена полная карточкаПолный ApartmentDetail со статусом и метриками, analyticsAvailable = trueконтракт-тест
AC2Агент с catalog.commissions.view vs инвесторЗапрошена карточкаАгент видит agentCommission; без permission поле отсутствует в JSON (не null-заглушка)два актора, сравнение сериализации
AC3Объект без правила rewards (AgentCommission = null)Запрошена карточка агентомБлок комиссии в ответе отсутствуетобъект без правила rewards
AC4Карточки во всех статусах доступностиОтрисована карточкаКнопка «Забронировать» активна только при propertyObjectStatus = Freeкарточки Free/Reserved/Booked/Sold
AC5Метрики синхронизированыЗапрошена карточкаЗначения метрик = рассчитанные консюмером invest-analyticsсверка с событием property-object-metrics
AC6Пользователь выполняет калькулятор/PDF/избранное/броньДействие отправленоВ user-activity созданы типизированные ActionEventпроверка записей
AC7Объект сменил статус в chessboardКарточка перечитанаНовый статус виден в карточке с лагом < 1 мининтеграционный тест реплики
AC8JWT отсутствует/истёкОткрыта полная карточкаПереход в UC-CTL-007 (ограниченная карточка / вход), 401 Unauthorized (A1)запрос без JWT
AC9id — не GuidЗапрошена карточкаValidationError (400)запрос с битым id
AC10invest-analytics/favorites/crm недоступенОткрыта карточка / выполнено действиеДеградирует только соответствующий блок, ядро карточки остаётся (A6)отказ каждой зависимости
AC11Объект ушёл в Reserved конкурентноОтправлена бронь по устаревшему Freecrm отклоняет на источнике; каталог ошибок не генерирует; UI обновляет статус (A5)гонка статуса
AC12Объект уже в избранном / уже забронированПовторное «В избранное» / повторная броньДубликат не создаётся (идемпотентность favorites/crm), пользователь видит текущее состояние (A7)повторные действия

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

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

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

  • API метод: ../api/public/get-property-object-by-id.md.
  • Domain entity: ../domain/catalog-property-object-card.md (ApartmentDetail, PropertyObjectAgentCommission, PropertyObjectAvailabilityStatus, INV-5 CommissionConsistency).
  • Аналитика/калькулятор/PDF: ../../invest-analytics/ (график роста, серии, отчёт).
  • Комиссии: ../../rewards/domain/reward-rule.md (дом правил вознаграждений).
  • Бронирование: ../../crm/ (booking; заявка «Забронировать», проверка статуса в источнике).
  • Избранное/подборки: ../../favorites/, ../../selections/.
  • Просмотры/действия: ../../user-activity/ (ADR-0054/0056), ../../dev-stats/ (ADR-0057).
  • Связанные UC: UC-CTL-007 (ограниченная), UC-CTL-006 (карточка проекта), UC-CTL-009 (шахматка).