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

Entity / Aggregate: CatalogProjectCard

Дом полей денормализованной карточки проекта (ЖК/апарт-отеля) публичного каталога (контекст catalog). Модели ответов public API (лента /hotels, карточка /hotels/[id]/info), payload событий и поисковые проекции ссылаются сюда и описывают только дельту.

Назначение

CatalogProjectCardденормализованная витринная проекция проекта застройщика для ленты проектов (главная страница), поиска, фильтрации, сортировки и ограниченной/полной карточки проекта. Карточка отвечает на запрос ленты одной строкой без джойнов в источники: название, адрес, девелопер, диапазон цен, количество объектов, окупаемость.

Правило «только дельта + ссылка на дом chessboard»

Структура проекта (декларации, ход строительства, банки, медиа, шахматка) — дом chessboard/domain/project.md; этот документ владеет только витринной дельтой: эффективными значениями после корпусных оверрайдов, агрегатами по объектам, метриками invest-analytics и ранжированием. Полная карточка проекта (вкладки Информация/Локация/Финансирование/ Юридическая информация/Ход строительства/Видео/Карта и шахматка) читается из chessboard; каталог отдаёт только витринную часть и id для перехода. Полнотекстовый поиск проектов — Elasticsearch-проекция (../search/projects.md, ADR-0059).

Эффективные значения корпусного уровня: Effective = BuildingOverride(поле главного корпуса) ?? SourceSnapshot(поле); агрегаты (цены/количества/окупаемость) пересчитываются по Published-объектам (catalog-property-object-card.md).

Классификация

ПолеЗначение
Контекстcatalog
KindReplica (денормализованная read-проекция; мутируется консюмерами + пересчётами)
Базовые контрактыреплика-проекция, не агрегат
Source of truthструктура проекта — chessboard; имя девелопера — identity (через реплику chessboard); метрики — invest-analytics; эффективные display-значения, агрегаты и ранжирование — этот контекст
Таблицаcatalog.project_cards
Tenant isolationне tenanted — платформенная витрина (ADR-0052 п.3); Developer.DeveloperId — колонка для фильтров/ranking-scope
Search projectionElasticsearch projects (ADR-0059)

Полнотекстовый поиск обеспечивает Elasticsearch-документ (PostgreSQL-FTS не используется).

Поля

Легенда источников — как в catalog-property-object-card.md: chessboard / invest-analytics / override (display-override.md, корпусные) / identity (через реплику chessboard) / catalog (вычисление).

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKchessboard (Project.Id)Идентификатор проекта = id в chessboard.
DeveloperDeveloperRefModel (owned)Дасм. VO; index ix_project_cards_developer_idchessboard (реплика pin.identity.companies.v1)Застройщик (id + имя, = CompanyId в identity, ADR-0052); имя индексируется в поисковой проекции.
DeveloperLogoFileIdGuid?Нетбез FK (storage/CDN)identity → chessboardЛоготип застройщика.
NamestringДаvarchar(256)override (ComplexName) → chessboardЭффективное название комплекса/отеля (BuildingOverride.ComplexName ?? SourceSnapshot.Name); индексируется в поисковой проекции.
KindProjectKindДахранится как text, varchar(32), index ix_project_cards_kindchessboardResidentialComplex (ЖК) / ApartHotel (апарт-отель) — фильтр «тип» и разведение лент /lots vs /hotels.
PropertyClassPropertyClassДахранится как text, varchar(32), index ix_project_cards_classchessboardКласс: Comfort / Business / Premium / Elite — фильтр «класс».
RegionIdlong?Нетindex ix_project_cards_regionchessboard (RegionId события)Регион — ссылка на справочник handbook, ключ фильтра «регион». null, пока источник не резолвил гео (у Published заполнен — публикация в источнике требует резолва, INV-PRJ-3 chessboard).
RegionNamestring?Нет (заполнен ⇔ RegionId != null)varchar(256)chessboard (RegionSnapshot события, snapshots.md)Отображаемое имя региона (денормализовано из снапшота события).
CityIdlong?Нетindex ix_project_cards_citychessboard (CityId события)Город — ссылка на справочник handbook, ключ фильтра «город». null, пока источник не резолвил гео (у Published заполнен, INV-PRJ-3 chessboard).
CityNamestring?Нет (заполнен ⇔ CityId != null)varchar(256)chessboard (CitySnapshot события, snapshots.md)Отображаемое имя города (денормализовано из снапшота события).
AddressstringДаvarchar(512)override → chessboardАдрес; индексируется в поисковой проекции.
GeoLocationProjectGeoLocation (owned)Нетсм. VOchessboardКоординаты для карты.
ConstructionStatusConstructionStatusДахранится как text, varchar(32), index ix_project_cards_constructionchessboardPlanned / InProgress / Commissioned — фильтр «статус строительства».
DeliveryDateDateOnly?Нетoverride → chessboardСрок сдачи (ближайший по корпусам либо оверрайд).
ReadinessPercentshort?Нет0..100override → chessboardГотовность строительства.
Starsshort?Нет1..5override → chessboardЗвёздность апарт-отеля.
DistanceMinutesint?Нет>= 0override → chessboardМинуты до ключевой точки.
CoverFileIdGuid?Нетбез FK (storage/CDN)chessboardОбложка карточки.
ShortDescriptionstring?Нетvarchar(1024)chessboardКраткое описание для ленты/подсказок при наведении.
PriceNullableRange<decimal>НетMin/Max decimal(14,2), Min > 0, Max >= Min (check); index ix_project_cards_min_price (status, price_min)catalog (агрегат)Диапазон цен по Published-объектам проекта (Min=минимум, Max=максимум); null — нет доступных объектов. Диапазон цен ленты + фильтр «ценовой диапазон» + сортировка «по цене».
PropertyObjectsTotalintДа>= 0, default 0catalog (агрегат)Всего объектов проекта в каталоге (кроме Delisted).
PropertyObjectsAvailableintДа0 <= PropertyObjectsAvailable <= PropertyObjectsTotal (check)catalog (агрегат)Published-объекты с PropertyObjectStatus = Free — «количество объектов» в ленте. Для гостя точные статусы скрываются на уровне ответа API, не хранения.
PaybackPropertyObjectPayback (owned, переиспользуется)Нетсм. catalog-property-object-card.mdcatalog (агрегат из invest-analytics)Лучшая (минимальная в годах) окупаемость по объектам проекта — «окупаемость» в ленте.
GrowthPercentdecimal(5,2)?Нет-99.99..999.99invest-analyticsПрогнозный рост цены по проекту, %/год.
Ratingdecimal(3,1)?Нет0.0..10.0invest-analyticsРейтинг проекта.
DeveloperPriorityintДа0..1000, default 0catalog (ranking-rule.md)Приоритет проекта внутри объектов застройщика.
PromotionBoostintДа>= 0, default 0consumer promotion (../consumers/promotion-boost.md)Суммарный вес активных пакетов продвижения на проект; наполняется консюмером pin.promotion.purchase-changed.v1 (не владеется каталогом, ADR-0060). Первый ключ сортировки «Рекомендованные» (ranking-rule.md) — ранжирование остаётся у каталога.
PopularityViewslongДа>= 0, default 0catalog (consumer pin.user-activity.property-object-view-ended.v1, агрегация по ProjectId)Счётчик завершённых просмотров объектов проекта.
PopularityScoredecimal(12,4)Да>= 0, default 0, index ix_project_cards_popularitycatalog (job)Сглаженная популярность (окно 30 дней) — сортировка «по популярности».
StatusProjectCardStatusДахранится как text, varchar(32), index ix_project_cards_statuscatalog (INV-1)Published / Hidden / Delisted.
SourceSnapshotProjectSourceSnapshotDocument (jsonb)Даtyped jsonb, ContractVersionconsumer-ыСырые значения chessboard до оверрайдов (см. JSONB).
PublishedAtDateTimeOffset?Нетindex ix_project_cards_published_atcatalogПервая публикация — сортировка «по дате добавления».
ChessboardSyncedAtDateTimeOffsetДаconsumerПрименение последнего события chessboard.
AnalyticsSyncedAtDateTimeOffset?НетconsumerТо же для invest-analytics.
AggregatesRecomputedAtDateTimeOffsetДаcatalogПоследний пересчёт Price/PropertyObjectsTotal/PropertyObjectsAvailable/Payback.
CreatedAtDateTimeOffsetДаappСоздание проекции.
UpdatedAtDateTimeOffsetДаappПоследний пересчёт.

Enum ProjectCardStatus

ЗначениеОписание
PublishedПроект в ленте/поиске. В ленту попадают только опубликованные в источнике проекты.
HiddenСкрыт: chessboard прислал неопубликованный статус проекта либо корпусный оверрайд IsVisible=false скрывает все корпуса. Объекты проекта каскадно Hidden (INV-1 property-object-card).
DelistedПроект удалён в источнике; очищается retention-job-ом через 30 дней вместе с объектами.

Enum ProjectKind

ЗначениеОписание
ResidentialComplexЖилой комплекс (лента /lots).
ApartHotelАпарт-отель (лента /hotels).

Enum PropertyClass

ЗначениеОписание
ComfortКомфорт-класс.
BusinessБизнес-класс.
PremiumПремиум.
EliteЭлитный.

Enum ConstructionStatus

ЗначениеОписание
PlannedКотлован/старт продаж, строительство не начато.
InProgressСтроится.
CommissionedСдан/введён в эксплуатацию.
Unknown-значения enum

Unknown-значения enum из событий источника маппятся консюмером в ближайшее известное + warning-лог.

Value objects

ProjectGeoLocation (owned)

ПолеТипОбязательностьОграниченияКолонка
Latitudedecimal(9,6)Да (внутри VO)-90.000000..90.000000geo_latitude
Longitudedecimal(9,6)Да-180.000000..180.000000geo_longitude

DeveloperRefModel (DeveloperId/DeveloperName) и PropertyObjectPayback (payback_value, payback_period) переиспользуются из catalog-property-object-card.md. PriceNullableRange<decimal> с колонками price_min/price_max.

Модели JSONB (versioned)

ProjectSourceSnapshotDocument (jsonb, колонка source_snapshot, ContractVersion = 1)

ПолеТипОбязательностьОграниченияSince version
ContractVersionintДа= 1v1
NamestringДаvarchar(256); исходное название проектаv1
AddressstringДаvarchar(512)v1
Starsshort?Нет1..5v1
DeliveryDateDateOnly?Нетближайшая по корпусамv1
ReadinessPercentshort?Нет0..100v1
DistanceMinutesint?Нет>= 0v1
ChessboardVersionlongДа>= 1; guard монотонности (INV-3)v1
AnalyticsAsOfDateTimeOffset?Нетмомент расчёта метрик проектаv1

Связи

СвязьТипКлючПравило загрузкиDelete behavior
CatalogPropertyObjectCard (объекты проекта)reference (обратная, логическая)property_object_cards.project_id → project_cards.idпересчёт агрегатов: агрегация по объектам проекта (ProjectId, Status=Published)скрытие/делистинг проекта каскадно пересчитывает Status объектов
BuildingDisplayOverride (корпуса проекта)reference (логическая)building_display_overrides.project_id → project_cards.idкорпусные оверрайды при пересчёте эффективных полейудаление оверрайда → пересчёт из SourceSnapshot
PromotionContribution (вклады продвижения)reference (логическая, consumer)TargetKind=Project, TargetId → project_cards.idпересчёт PromotionBoost = Σ активных вкладов (../consumers/promotion-boost.md)истечение/отмена (событие promotion) → пересчёт
chessboard.Projectreplica-источникIdconsumer pin.chessboard.projects.v1delete-событие → Delisted

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-1 VisibilityRuleStatus = Published ⇔ проект опубликован в источнике ∧ не скрыт оверрайдами всех корпусовпересчёт видимости в консюмерах и пересчёте оверрайдовнет — детерминированный пересчёт
INV-2 AggregatesConsistencyPrice/PropertyObjectsTotal/PropertyObjectsAvailable/Payback соответствуют текущему множеству объектов; пересчёт — в той же транзакции, что изменившая объект операция, по внутреннему событию пересчёта PropertyObjectCardRecomputedEventобработчик события пересчёта (агрегация по ProjectId)нет
INV-3 MonotonicChessboardСобытие применяется только при event.Version > SourceSnapshot.ChessboardVersionconsumerнет — пропуск
INV-4 KindImmutableKind не меняется после создания (ЖК не становится отелем); расхождение с источником — конфликт в лог + алертconsumer guardнет — событие в DLQ при систематическом расхождении
INV-5 CascadeHideПереход Published → Hidden/Delisted каскадно пересчитывает Status всех объектов проекта в той же транзакцииобработчик переходанет

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
ProjectCardRecomputedEventдоменноеПересчёт эффективных полей/статуса/агрегатовId, Status, Price, PropertyObjectsAvailable

Интеграционные события карточка не публикует (см. обоснование в catalog-property-object-card.md, раздел «События»).

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK pk_project_cards (id); ленточный композит ix_project_cards_feed (status, promotion_boost desc, developer_priority desc, popularity_score desc, published_at desc) where status = 'Published'; btree: ix_project_cards_developer_id, ix_project_cards_kind, ix_project_cards_class, ix_project_cards_region (region_id), ix_project_cards_city (city_id), ix_project_cards_construction, ix_project_cards_min_price (status, price_min), ix_project_cards_published_at (status, published_at desc), ix_project_cards_popularity (status, popularity_score desc)
Поискполнотекст — Elasticsearch-проекция projects (ADR-0059); в PostgreSQL FTS-колонок/GIN нет
Партиционирование / retentionnone — проектов сотни/тысячи; Delisted очищаются через 30 дней
Concurrencyоптимистическая блокировка, retry консюмеров при конфликте
Permissions / PIIЧтение — public (ограниченная карточка без авторизации разрешена; лента /lots — гейт токен/реф-cookie); для гостя ответ API маскирует точные цены и статусы объектов — правило маскирования живёт в API-методах, не в хранении. PII нет
AuditНе требуется (read-проекция); trace — SourceSnapshot + *SyncedAt

Ожидания от источников и согласованность

Карточка — реплика: её поля наполняют консюмеры соседних контекстов, поэтому целостность витрины держится на гарантиях версий и порядка, а не на синхронных запросах в чужие контексты.

ИсточникСобытие (топик)Что даётГарантия примененияПоведение при отсутствии/устаревании
chessboardpin.chessboard.projects.v1структура, статус публикации в источнике, SourceSnapshot, Kind, гео-ссылки + display-имена (RegionId+RegionName, CityId+CityName — из id и снапшотов события, собственной реплики справочника у catalog нет)монотонность по ChessboardVersion (INV-3): событие с Version <= уже применённого пропускаетсянет события проекта → карточки нет (объекты проекта висят Hidden, INV-2 объекта); устаревшее → показываются прежние значения до прихода более нового; переименование региона/города в справочнике доезжает штатной переопубликацией события источником (HR-6 chessboard)
identity (через реплику chessboard)pin.identity.companies.v1имя/лого застройщикаприходит вместе со снапшотом проектапереименование застройщика видно после доезда события; до него — прежнее имя
invest-analyticspin.invest-analytics.property-object-metrics.v1агрегаты GrowthPercent/Rating/Payback (лучшее по объектам)AnalyticsSyncedAt — водяной знак свежестине синхронизировано → метрики null, блок в UI скрыт (деградация, не ошибка)
promotionpin.promotion.purchase-changed.v1PromotionBoost (ADR-0060)пересчёт суммарного веса активных пакетов консюмеромлаг/недоступность продвижения → буст устаревает, порядок «Рекомендованные» слегка неточен (некритично)
user-activitypin.user-activity.property-object-view-ended.v1PopularityViews (агрегат по проекту)«слепой» инкремент, fail-open (../consumers/user-activity-popularity.md)потеря единичного просмотра допустима; PopularityScore пересчитывает ночной job
Окно согласованности

Любое изменение объекта проекта → доменное PropertyObjectCardRecomputedEvent → пересчёт агрегатов карточки проекта (INV-2) в той же транзакции; в том же коммите поднимается ProjectCardRecomputedEvent, обновляющий поисковую проекцию (../search/projects.md). Читатели ленты/поиска в интервале секунды–минуты (лаг консюмеров + индексация) видят прежние значения цен/агрегатов; отдаётся признак свежести. Критичные для брони проверки статуса делает crm в источнике, а не витрина.

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

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

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