UC-CTL-008. Карточка объекта (полная: аналитика, калькулятор, PDF, избранное, бронь)
Назначение
Авторизованный пользователь открывает полную карточку объекта недвижимости: все характеристики (площадь,
этаж, комнатность, цена, статус), инвестиционная аналитика (график роста цены, прогноз доходности, ROI,
окупаемость), калькулятор, PDF-отчёт, добавление в избранное/подборку, бронирование. Карточку и комиссию
агента отдаёт каталог (CatalogPropertyObjectCard); аналитику/калькулятор/PDF — invest-analytics,
избранное — favorites, подборки — selections, бронирование — crm; действия записываются в
user-activity.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-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) авторизованным пользователем |
Основной Поток
- Пользователь открывает полную карточку объекта; интерфейс проверяет JWT (нет — UC-CTL-007 A1).
- Интерфейс запрашивает карточку объекта по
id— та же операция, что UC-CTL-007, но для авторизованного актора без отсечения полей:- Проверка входа/загрузка.
id— Guid,Status = Published(иначе 404CATALOG_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 }— комиссия агента на странице объекта (VOPropertyObjectAgentCommission, денормализована консюмеромpin.rewards.commission-rules.v1). - Результат — карточка.
- Проверка входа/загрузка.
- Интерфейс открывает вкладку аналитики
/lots/[id]/analyticsи запрашиваетinvest-analytics(его API —docs/06-services/invest-analytics/): график роста цены по истории цен (ClickHouse), прогноз доходности/ROI/окупаемость, серииInvestorMonthс параметрами юр/физлицо + УСН/ОСНО. - Пользователь использует калькулятор ипотеки и доходности: ввод параметров → серверный пересчёт в
invest-analyticsв реальном времени; программы покупки/график платежей —crm(booking). - Пользователь скачивает PDF-отчёт по объекту: интерфейс вызывает
invest-analytics(формирование отчёта и отдача файла). - Пользователь добавляет объект в избранное или подборку: вызовы
favorites/selectionsпоpropertyObjectId(карточки живут по id и переживают делистинг —Delisted-retention 30 дней). Дома сценариев:docs/06-services/favorites/,docs/06-services/selections/. - Пользователь нажимает «Забронировать» (доступно при
propertyObjectStatus = Free): форма ФИО+телефон уходит вcrm(booking/leads; каталог заявку не обрабатывает). Смена статуса вернётся в карточку цепочкойcrm → chessboard → pin.chessboard.property-objects.v1 → catalog. - Запись действий в аудит: интерфейс шлёт в
user-activityоткрытиеPropertyObjectView(+heartbeat, как UC-CTL-007) и типизированныеActionEvent-ы действий (CalculatorUsed,PdfDownloaded,AddedToFavorites,BookingRequested) — пробрасываются в карточку лида агента (crm) и статистику застройщика (dev-stats).
Альтернативные Потоки
A1. Пользователь не авторизован
- Переход к UC-CTL-007 (ограниченная карточка с CTA входа); после входа — возврат к шагу 2.
A2. Актор — агент (комиссия)
- Шаг 2 включает
agentCommissionв ответ (диапазонpercentMin..percentMax, когда матчится несколько правил застройщика — тип/планировка/группа/комнатность/площадь). - Если комиссия для объекта недвижимости не задана (
AgentCommission = null), блок не отображается (правило VO — catalog-property-object-card.md). Поток не меняется.
A3. Объект недвижимости недоступен для брони (Reserved/Booked/Sold)
- Кнопка «Забронировать» неактивна; отображается статус и, для
Reserved, таймер (дом переходов статуса —chessboard; глоссарий «Статус объекта»). Шаг 7 пропускается.
A4. Метрики аналитики ещё не синхронизированы
Payback/GrowthPercent/RentalIncomePerYear= null (AnalyticsSyncedAtпуст) — карточка отображается без блока метрик; вкладка аналитики запрашиваетinvest-analyticsнапрямую и может отдать данные раньше витрины (лаг консюмера метрик < 15 мин — README «SLO»). Поток не меняется.
A5. Конкурентная смена статуса во время просмотра
- Пользователь смотрит карточку, объект недвижимости уходит в
Reserved(другой инвестор). Каталог отдаёт реплику на момент запроса; попытка брони по устаревшемуFreeотклоняетсяcrmпроверкой в источнике — каталог ошибок не генерирует. Интерфейс обновляет статус по ответуcrm.
A6. Частичный отказ зависимости блока (аналитика/избранное/подборки/бронь)
- Ядро карточки (шаг 2, каталог) уже отдано. Отказ одного из соседних контекстов деградирует только
свой блок, карточка остаётся:
invest-analyticsнедоступен/таймаут → вкладка аналитики и калькулятор показывают skeleton + повтор; агрегатные метрики карточки (если синхронизированы) остаются, т.к. денормализованы в витрине;favorites/selectionsнедоступны → действие «В избранное/подборку» возвращает ошибку своего контекста, тост об ошибке, карточка активна;crmнедоступен → форма брони показывает ошибку и предлагает повтор; статус объекта не меняется.
- Пользователь может продолжать работать с остальной карточкой. Сценарий завершается частично успешно.
A7. Повторные действия (идемпотентность)
- Повторное «В избранное» по тому же
propertyObjectIdне создаёт дубликат —favoritesидемпотентен по паре (пользователь, объект); интерфейс отражает уже добавленное состояние. - Повторная заявка «Забронировать» по тому же объекту дедуплицируется
crm(существующие лид/бронь); пользователь видит уже созданную заявку, второго лида не появляется. - Повторная доставка
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включается только при permissioncatalog.commissions.view(иначе поле отсутствует в JSON, неnull); диапазонpercentMin..percentMaxпоявляется, когда матчатся несколько правил застройщика (тип/планировка/группа/комнатность/площадь);AgentCommission = null→ блок комиссии отсутствует даже для агента. - Доступность брони. Кнопка «Забронировать» активна только при
propertyObjectStatus = Free;Reserved/Booked/Sold→ неактивна (A3), дляReservedпоказывается таймер; форма брони (ФИО + телефон) валидируется и обрабатываетсяcrm, каталог заявку не хранит. - Деградация метрик. Пусто
AnalyticsSyncedAt→Payback/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проверяет статус на источнике и создаёт бронь/лид →crm→chessboard(смена статуса) →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.v1→crm(карточка лида агента),dev-stats(статистика застройщика). Просмотр (PropertyObjectView) и его завершение — цепочкаpin.user-activity.property-object-view-ended.v1→PopularityViewsкаталога иdev-stats, как в UC-CTL-007.
Постусловие
- Пользователь видит полную карточку с аналитикой; выполненные действия (калькулятор, PDF, избранное, заявка на бронь) обработаны целевыми контекстами.
- В
user-activity—PropertyObjectViewиActionEvent-ы всех действий; после закрытия просмотраproperty-object-view-endedинкрементируетPopularityViewsкарточки. - Состояние каталога не изменилось (мутации — в
favorites/selections/crm/user-activity).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | id — не Guid | Страница ошибки |
Unauthorized | 401 | JWT отсутствует/истёк | A1: ограниченная карточка / вход |
CATALOG_PROPERTY_OBJECT_NOT_FOUND | 404 | Карточки нет или 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 | приём действий шага 8 | crm (карточка лида) |
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-класс данных, только по permissioncatalog.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 мин | интеграционный тест реплики |
| AC8 | JWT отсутствует/истёк | Открыта полная карточка | Переход в UC-CTL-007 (ограниченная карточка / вход), 401 Unauthorized (A1) | запрос без JWT |
| AC9 | id — не Guid | Запрошена карточка | ValidationError (400) | запрос с битым id |
| AC10 | invest-analytics/favorites/crm недоступен | Открыта карточка / выполнено действие | Деградирует только соответствующий блок, ядро карточки остаётся (A6) | отказ каждой зависимости |
| AC11 | Объект ушёл в Reserved конкурентно | Отправлена бронь по устаревшему Free | crm отклоняет на источнике; каталог ошибок не генерирует; UI обновляет статус (A5) | гонка статуса |
| AC12 | Объект уже в избранном / уже забронирован | Повторное «В избранное» / повторная бронь | Дубликат не создаётся (идемпотентность favorites/crm), пользователь видит текущее состояние (A7) | повторные действия |
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- 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 (шахматка).