UC-CTL-007. Карточка объекта (ограниченная)
Назначение
Гость с реф-cookie или пользователь без полного доступа открывает карточку объекта недвижимости и видит
основную информацию: описание, цена, срок сдачи, общая и жилая площадь, этаж, застройщик. Все поля
каталог отдаёт одной строкой витрины CatalogPropertyObjectCard (модель ApartmentDetail); детальная
инвестиционная аналитика и история цен скрываются. Просмотр записывается в user-activity.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-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 |
Основной Поток
- Пользователь кликает по объекту в списке; интерфейс открывает
/lots/[id]/infoи проверяет гейт (JWT или реф-cookie; нет обоих — A1). - Интерфейс запрашивает карточку объекта по
id. - Каталог формирует ограниченную карточку:
- Проверка входа.
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). - Результат — карточка.
- Проверка входа.
- Интерфейс отображает страницу с основной информацией (описание, цена, срок сдачи, площади, этаж, застройщик) и предложением войти для полной детализации (CTA «Войдите, чтобы увидеть аналитику»).
- Интерфейс записывает просмотр в историю: открывает
PropertyObjectView{ targetKind = PropertyObject, targetId = id, sessionId, userId? / anonymous_id, filterContext (активные фильтры ленты) }сCreatedAt; heartbeat интерфейса обновляетLastActivityAt; закрытие и длительность — polling-workeruser-activity(ADR-0056 п.3).
Альтернативные Потоки
A1. Нет ни JWT, ни реф-cookie
- Гейт не пройден → 401
CATALOG_FEED_GATE_REQUIRED; интерфейс показывает предложение войти и открывает auth-модалку (UC-IDN-001). - После входа — возврат к шагу 2.
A2. Пользователь авторизован
- Шаг 3 отдаёт полный набор полей (
analyticsAvailable = true). - Продолжение — UC-CTL-008 (полная карточка с аналитикой). Просмотр записывается с
userId.
A3. Объект недвижимости не найден / скрыт / делистнут
- Шаг 3 →
CATALOG_PROPERTY_OBJECT_NOT_FOUND(404); интерфейс — страница «Объект недоступен» со ссылкой на проект/ленту. Сценарий завершается.
Карточка переживает удаление 30 дней в статусе Delisted, но публично не отдаётся — попадает в тот же
поток A3, что и несуществующий или скрытый объект.
A4. Статус объекта недвижимости изменился (Резерв/Бронь/Продан)
propertyObjectStatusв ответе отражает последнюю репликуpin.chessboard.property-objects.v1(лаг < 1 мин — README «SLO»); кнопка «Забронировать» активна только приFree(отображение — enumPropertyObjectAvailabilityStatus, catalog-property-object-card.md).- Критичную проверку статуса при бронировании выполняет
crmв источнике — каталог только отображает. Поток не меняется.
A5. Реферальная cookie просрочена или отозвана
- Гейт трактует невалидную (истёкшую/отозванную) реф-cookie как отсутствие гейта → 401
CATALOG_FEED_GATE_REQUIRED; интерфейс ведёт себя как в A1 (предложение войти, auth-модалка UC-IDN-001). После входа — возврат к шагу 2 уже с JWT (тогда набор полей расширяется — A2).
A6. user-activity недоступен при записи просмотра
- Шаги 1–4 отрабатывают, карточка показана. Шаг 5 (открытие
PropertyObjectView) — best-effort: при недоступностиuser-activityпросмотр не записывается, пользователю ошибка не показывается, популярность (PopularityViews) иdev-statsэтот показ не учитывают. Сценарий завершается частично успешно (карточка отдана).
A7. Оверрайд применён во время просмотра (конкурентный пересчёт витрины)
- Пока пользователь на странице, консюмер/пересчёт оверрайда меняет эффективные поля карточки
(цена/заголовок/бейдж/видимость). Каталог отдал снапшот на момент запроса; ошибок нет. Если пересчёт
перевёл карточку в
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не сериализуется (нет permissioncatalog.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 | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | id — не Guid | Страница ошибки |
CATALOG_FEED_GATE_REQUIRED | 401 | Нет JWT и нет реф-cookie | Auth-модалка (A1) |
CATALOG_PROPERTY_OBJECT_NOT_FOUND | 404 | Карточки нет или 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 мин), кнопка «Забронировать» неактивна | интеграционный тест реплики |
| AC11 | user-activity недоступен | Открыта карточка | Карточка показана без ошибки; просмотр не записан (best-effort, A6) | отказ зависимости |
| AC12 | id — не Guid | Запрошена карточка | ValidationError (400) | запрос с битым id |
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- 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).