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

UC-CTL-007. Карточка объекта (ограниченная)

Назначение

Гость с реф-cookie или пользователь без полного доступа открывает карточку объекта недвижимости и видит основную информацию: описание, цена, срок сдачи, общая и жилая площадь, этаж, застройщик. Все поля каталог отдаёт одной строкой витрины CatalogPropertyObjectCard (модель ApartmentDetail); детальная инвестиционная аналитика и история цен скрываются. Просмотр записывается в user-activity.

Metadata

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

Актор

ПолеЗначение
РольГость с реф-cookie (гейт как у ленты /lots) / Инвестор без полного доступа
Permissions / scopeгейт: JWT или реф-cookie; permissions не требуются; аналитика/история цен — только авторизованным
Tenantне применяется — платформенная витрина (ADR-0052 п.3)
Record-levelне применяется

Предусловие

  • CatalogPropertyObjectCard с запрошенным Id существует и Status = Published (INV-1/INV-2 карточки: проект опубликован, объект недвижимости не скрыт).
  • Гейт пройден: JWT или реферальная cookie.

Триггер

ПолеЗначение
ИсточникUI navigation
Триггерящий элементКлик по объекту в списке (лента /lots, ассортимент проекта, шахматка) либо прямая ссылка /lots/[id]/info

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

  1. Пользователь кликает по объекту в списке; интерфейс открывает /lots/[id]/info и проверяет гейт (JWT или реф-cookie; нет обоих — A1).
  2. Интерфейс запрашивает карточку объекта по id.
  3. Каталог формирует ограниченную карточку:
    • Проверка входа. id — Guid, иначе ValidationError (400). Гейт: актор авторизован ИЛИ реф-cookie валидна, иначе — CATALOG_FEED_GATE_REQUIRED (401). Карточка существует и Status = Published, иначе — CATALOG_PROPERTY_OBJECT_NOT_FOUND (404; Hidden/Delisted неотличимы от несуществующего).
    • Состав карточки ApartmentDetail (одна строка витрины; дом полей — catalog-property-object-card.md): id, number, title, typeName, view, badge, buildingId/buildingName, address, developer (застройщик), price (эффективная цена), deliveryDate (срок сдачи), areaTotal (общая площадь), areaLiving (жилая площадь), floor (этаж), floorsTotal, ceilingHeight, pricePerMeter, readinessPercent, stars, distanceMinutes, rating, payback, reliability, visualizationFileId (планировка; пресайн — граница storage/CDN), propertyObjectStatus.
    • Ограничение набора для гостя без JWT (реф-гость): поля детальной аналитики (growthPercent, rentalIncomePerYear) = null, analyticsAvailable = false — история цен/график роста/калькулятор недоступны (страница /lots/[id]/analytics в readOnly/CTA-режиме); agentCommission не включается (нет permission).
    • Результат — карточка.
  4. Интерфейс отображает страницу с основной информацией (описание, цена, срок сдачи, площади, этаж, застройщик) и предложением войти для полной детализации (CTA «Войдите, чтобы увидеть аналитику»).
  5. Интерфейс записывает просмотр в историю: открывает PropertyObjectView { targetKind = PropertyObject, targetId = id, sessionId, userId? / anonymous_id, filterContext (активные фильтры ленты) } с CreatedAt; heartbeat интерфейса обновляет LastActivityAt; закрытие и длительность — polling-worker user-activity (ADR-0056 п.3).

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

  1. Гейт не пройден → 401 CATALOG_FEED_GATE_REQUIRED; интерфейс показывает предложение войти и открывает auth-модалку (UC-IDN-001).
  2. После входа — возврат к шагу 2.

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

  1. Шаг 3 отдаёт полный набор полей (analyticsAvailable = true).
  2. Продолжение — UC-CTL-008 (полная карточка с аналитикой). Просмотр записывается с userId.

A3. Объект недвижимости не найден / скрыт / делистнут

  1. Шаг 3 → CATALOG_PROPERTY_OBJECT_NOT_FOUND (404); интерфейс — страница «Объект недоступен» со ссылкой на проект/ленту. Сценарий завершается.
Делистнутый объект в избранном/подборках

Карточка переживает удаление 30 дней в статусе Delisted, но публично не отдаётся — попадает в тот же поток A3, что и несуществующий или скрытый объект.

A4. Статус объекта недвижимости изменился (Резерв/Бронь/Продан)

  1. propertyObjectStatus в ответе отражает последнюю реплику pin.chessboard.property-objects.v1 (лаг < 1 мин — README «SLO»); кнопка «Забронировать» активна только при Free (отображение — enum PropertyObjectAvailabilityStatus, catalog-property-object-card.md).
  2. Критичную проверку статуса при бронировании выполняет crm в источнике — каталог только отображает. Поток не меняется.
  1. Гейт трактует невалидную (истёкшую/отозванную) реф-cookie как отсутствие гейта → 401 CATALOG_FEED_GATE_REQUIRED; интерфейс ведёт себя как в A1 (предложение войти, auth-модалка UC-IDN-001). После входа — возврат к шагу 2 уже с JWT (тогда набор полей расширяется — A2).

A6. user-activity недоступен при записи просмотра

  1. Шаги 1–4 отрабатывают, карточка показана. Шаг 5 (открытие PropertyObjectView) — best-effort: при недоступности user-activity просмотр не записывается, пользователю ошибка не показывается, популярность (PopularityViews) и dev-stats этот показ не учитывают. Сценарий завершается частично успешно (карточка отдана).

A7. Оверрайд применён во время просмотра (конкурентный пересчёт витрины)

  1. Пока пользователь на странице, консюмер/пересчёт оверрайда меняет эффективные поля карточки (цена/заголовок/бейдж/видимость). Каталог отдал снапшот на момент запроса; ошибок нет. Если пересчёт перевёл карточку в Hidden (INV-1 стал false), повторный запрос карточки вернёт CATALOG_PROPERTY_OBJECT_NOT_FOUND (404) — как A3.

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

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

  • Валидация входа. id — непустой Guid, иначе ValidationError (400). Гейт: валидный JWT или валидная реферальная cookie; отсутствие/просрочка/отзыв обоих — CATALOG_FEED_GATE_REQUIRED (401).
  • INV-1 VisibilityRule. Публично отдаётся только Status = Published. Hidden (оверрайд объекта/ корпуса IsVisible=false, PropertyObjectStatus=Hidden у источника или скрытый/неопубликованный проект) и Delisted (удалён в источнике, живёт до 30 дней) → тот же CATALOG_PROPERTY_OBJECT_NOT_FOUND (404), неотличимо от несуществующего id.
  • INV-3 EffectiveFields. Отображаемые значения — эффективные: Effective = Override(поле) ?? SourceSnapshot(поле); в частности price = Override.Price ?? SourceSnapshot.Price, title = Override.Title ?? SourceSnapshot.Title, pricePerMeter = round(price / areaTotal, 2).
  • Отсечение по доступу. Реф-гость без JWT: growthPercent = rentalIncomePerYear = null, analyticsAvailable = false; agentCommission не сериализуется (нет permission catalog.commissions.view) — это отсутствие поля, а не null-заглушка.
  • Статус доступности. propertyObjectStatus — реплика PropertyObjectAvailabilityStatus (Free/Reserved/Booked/Sold/Hidden); кнопка «Забронировать» активна только при Free. Дом статуса и переходов — chessboard.

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

Сборка карточки (что каталог ожидает от соседей). Строка витрины наполняется консюмерами заранее и в момент запроса читается без обращения в источники: структура/номер/этаж/площади/propertyObjectStatus — реплика chessboard (pin.chessboard.property-objects.v1, ключ партиционирования — PropertyObject.Id, порядок гарантируется version-guard INV-4: событие с Version <= ChessboardVersion игнорируется как устаревшее); метрики доходности — invest-analytics (реф-гостю не отдаются); эффективные display-значения и оверрайды — сам каталог. Устаревание реплики: propertyObjectStatus отстаёт от источника не более чем на ~1 мин (README «SLO»), метрики — до ~15 мин. В окне рассинхронизации пользователь может видеть неактуальную доступность, но критичная проверка при бронировании выполняется crm на источнике, поэтому ложной брони не происходит.

Запись просмотра (что клиент шлёт дальше, асинхронно). Открытие карточки — сигнал в user-activity (fire-and-forget, best-effort): создаётся PropertyObjectView { targetKind = PropertyObject, targetId = id, sessionId, userId? / anonymous_id, filterContext }; heartbeat интерфейса продлевает LastActivityAt. Цепочка завершения: polling-worker user-activity закрывает просмотр через 10–15 мин неактивности → публикует PropertyObjectViewEnded (pin.user-activity.property-object-view-ended.v1) → каталог инкрементит PopularityViews объекта и проекта (eventual; дедуп по id просмотра — повторная доставка события не задваивает счётчик), dev-stats агрегирует. Пользователь не видит собственный просмотр в счётчике популярности немедленно. Открытие одной карточки несколько раз создаёт несколько записей просмотра — это нормально (каждый показ = просмотр), задвоение исключается на уровне завершающего события.

Постусловие

  • Пользователь видит основную информацию об объекте без детальной аналитики/истории цен.
  • В user-activity открыт PropertyObjectView (гость — anonymous_id); после закрытия polling-ом длительность просмотра уйдёт в property-object-view-ended → популярность каталога и dev-stats.
  • Состояние каталога не изменилось (read-only).

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

КодHTTPУсловиеПоведение клиента
ValidationError400id — не GuidСтраница ошибки
CATALOG_FEED_GATE_REQUIRED401Нет JWT и нет реф-cookieAuth-модалка (A1)
CATALOG_PROPERTY_OBJECT_NOT_FOUND404Карточки нет или Status != PublishedСтраница «Объект недоступен»

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

СобытиеTopicКогда публикуетсяConsumers
PropertyObjectViewEnded (производит user-activity)pin.user-activity.property-object-view-ended.v1закрытие PropertyObjectView polling-ом через 10–15 мин неактивности (ADR-0056 п.3)catalog (PopularityViews объекта недвижимости и проекта), dev-stats

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

  • Логи (без PII, без токенов/cookie): property-object-card-served { cardId, variant=limited, gate=jwt|ref, analyticsAvailable=false }, card-gate-rejected { reason }, card-not-found { id }.
  • Метрики: catalog_property_object_card_requests_total{variant="limited",result}, доля 404 и 401 (аномальный рост 401 → проблема гейта/реф-cookie), латентность сборки карточки.
  • НФТ: карточка — чтение одной строки витрины без джойнов; целевая латентность p95 — единицы десятков мс на стороне каталога (нагрузка — до 10k одновременных, ADR-0056). Запись просмотра асинхронна и в бюджет ответа не входит.
  • Безопасность: гейт (JWT/реф-cookie) обязателен; agentCommission и детальная аналитика недоступны без соответствующих прав; PII в карточке нет.

Acceptance Criteria

#Given (контекст)When (действие)Then (результат)Проверка
AC1Опубликованный объект, актор прошёл гейтЗапрошена ограниченная карточкаВ ответе есть описание, цена, срок сдачи, общая/жилая площадь, этаж, застройщикконтракт-тест ответа
AC2Реф-гость без JWT vs авторизованныйЗапрошена карточкаГость: growthPercent/rentalIncomePerYear = null, analyticsAvailable = false; авторизованный — заполнены, analyticsAvailable = trueдва актора
AC3Опубликованный объектЗапрошена карточкаОтвет соответствует контракту ApartmentDetail (все поля детальной карточки)контракт-тест полей
AC4Карточка в витринеЗапрошена карточкаОтвет собран одной строкой property_object_cards (без джойнов и запросов в источники)проверка выборки
AC5Объект Hidden / Delisted / несуществующий idЗапрошена карточкаВо всех трёх — одинаковый CATALOG_PROPERTY_OBJECT_NOT_FOUND (404)три запроса
AC6Актор открывает карточку (гость — anonymous_id)Карточка открыта, страница активнаСоздан PropertyObjectView с filterContext; heartbeat продлевает LastActivityAtпроверка записей user-activity
AC7На карточке есть оверрайд ценыЗапрошена карточкаprice = Override.Price ?? SourceSnapshot.Price (INV-3)тест с оверрайдом
AC8Нет ни JWT, ни реф-cookieЗапрошена карточкаCATALOG_FEED_GATE_REQUIRED (401), клиент открывает auth-модалку (A1)запрос без credentials
AC9Реф-cookie просрочена/отозванаЗапрошена карточкаГейт не пройден → 401 (A5), как при отсутствии cookieзапрос с невалидной cookie
AC10Объект перешёл в Reserved (другой инвестор)Карточка перечитанаpropertyObjectStatus = Reserved в пределах лага реплики (~1 мин), кнопка «Забронировать» неактивнаинтеграционный тест реплики
AC11user-activity недоступенОткрыта карточкаКарточка показана без ошибки; просмотр не записан (best-effort, A6)отказ зависимости
AC12id — не GuidЗапрошена карточкаValidationError (400)запрос с битым id

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

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

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

  • API метод: get-property-object.md (GET /api/v1/lots/{id}, ограниченный/полный режим ответа).
  • Domain entity: ../domain/catalog-property-object-card.md (дом полей ApartmentDetail, статусы, эффективные значения), ../domain/display-override.md.
  • Просмотры: ../../user-activity/domain/property-object-view.md (дом записи, ADR-0054/0056).
  • Связанные UC: UC-CTL-008 (полная карточка), UC-CTL-001 (лента), UC-IDN-001 (вход по CTA).