Entity / Aggregate: PropertyObjectDisplayOverride + BuildingDisplayOverride
Дом полей оверрайдов отображения объекта недвижимости и корпуса (контекст catalog).
Назначение
Оверрайд — локальная правка витринных данных объекта недвижимости/корпуса поверх данных источника
(структура недвижимости из контекста chessboard). Администратор платформы (через admin API, ADR-0053)
корректирует отображение: скрывает объект, поправляет цену/заголовок/бейдж/рейтинг объекта, название
комплекса/звёздность/срок сдачи/готовность корпуса — не трогая source of truth в chessboard.
Оверрайд хранит только переопределённые поля (все override-поля nullable; null = «поле не
переопределено»). Эффективное значение витрины: Effective = Override(поле) ?? SourceSnapshot(поле) —
правило INV-3 catalog-property-object-card.md. Применение/изменение/
удаление оверрайда пересчитывает связанные карточки в той же транзакции (один контекст, одна БД,
simplicity-first — ADR-0056).
Классификация
| Поле | Значение |
|---|---|
| Контекст | catalog |
| Kind | оба — AggregateRoot (самостоятельный жизненный цикл, admin-мутации, аудит) |
| Source of truth | этот контекст (оверрайд — собственные данные PIN поверх реплики) |
| Таблицы | catalog.property_object_display_overrides, catalog.building_display_overrides |
| Tenant isolation | не tenanted — правки платформы, управляются администратором платформы (ADR-0053) |
Поля PropertyObjectDisplayOverride
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор оверрайда. |
PropertyObjectId | Guid | Да | unique ux_property_object_overrides_property_object_id where deleted_at is null; логическая ссылка на catalog.property_object_cards.id (без FK — оверрайд может быть создан до прихода карточки, INV-2) | admin API | Объект недвижимости, чьё отображение правится. На объект — максимум один живой оверрайд (INV-1). |
IsVisible | bool? | Нет | — | admin | false — скрыть объект из витрины; true — принудительно показать (перекрывает только витринную видимость, не PropertyObjectStatus=Hidden источника — INV-1 property-object-card требует оба условия); null — не переопределено. |
Price | decimal(14,2)? | Нет | > 0 (check ck_property_object_overrides_price) | admin | Витринная цена вместо цены источника. |
Title | string? | Нет | varchar(512), min 1 после trim | admin | Заголовок карточки. |
Badge | string? | Нет | varchar(64) | admin | Маркетинговый бейдж. |
ReliabilityValue | short? | Нет | 0..ReliabilityMax; пара с ReliabilityMax (check: оба null или оба заданы) | admin | Значение индекса надёжности. |
ReliabilityMax | short? | Нет | 1..100 | admin | Максимум шкалы надёжности. |
PaybackValue | decimal(5,1)? | Нет | > 0; пара с PaybackPeriod (check) | admin | Значение окупаемости. |
PaybackPeriod | PaybackPeriod? (enum-string) | Нет | Years/Months, varchar(16) (enum — дом catalog-property-object-card.md) | admin | Единица окупаемости. |
Rating | decimal(3,1)? | Нет | 0.0..10.0 | admin | Рейтинг карточки. |
Comment | string? | Нет | varchar(1024) | admin | Служебный комментарий администратора «зачем правка» (не отдаётся в public API). |
CreatedAt | DateTimeOffset | Да | index ix_property_object_overrides_created_at | app | — |
CreatedByUserId | Guid? | Да (для admin-мутаций) | без FK (admin вне identity, ADR-0053) | app | Автор правки. |
UpdatedAt | DateTimeOffset | Да | — | app | — |
UpdatedByUserId | Guid? | Нет | — | app | — |
DeletedAt | DateTimeOffset? | Нет | partial-фильтр рабочих индексов | app | Soft-delete = «оверрайд снят». |
DeletedByUserId | Guid? | Нет | — | app | — |
Поля BuildingDisplayOverride
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор оверрайда. |
BuildingId | Guid | Да | unique ux_building_overrides_building_id where deleted_at is null | admin API | Корпус chessboard. Один живой оверрайд на корпус (INV-1). |
ProjectId | Guid | Да | index ix_building_overrides_project_id; логическая ссылка на catalog.project_cards.id | admin API (резолвится из карточки корпуса при создании) | Денормализованный проект — пересчёт карточки проекта без обратного lookup-а. |
IsVisible | bool? | Нет | — | admin | false — скрыть корпус: все объекты корпуса → Hidden (INV-1 property-object-card). |
ComplexName | string? | Нет | varchar(256), min 1 после trim | admin | Название комплекса — эффективное Name карточки проекта. |
Stars | short? | Нет | 1..5 | admin | Звёздность отеля. |
DeliveryDate | DateOnly? | Нет | — | admin | Срок сдачи. |
ReadinessPercent | short? | Нет | 0..100 | admin | Готовность строительства. |
Address | string? | Нет | varchar(512) | admin | Правка адреса витрины. |
DistanceMinutes | int? | Нет | >= 0 | admin | Минуты до ключевой точки. |
Comment | string? | Нет | varchar(1024) | admin | Служебный комментарий. |
CreatedAt / CreatedByUserId / UpdatedAt / UpdatedByUserId / DeletedAt / DeletedByUserId | — | — | как у PropertyObjectDisplayOverride | app | Audit/soft-delete — идентично. |
Value objects
Отдельных value objects нет. Пары «значение+шкала» (ReliabilityValue/Max, PaybackValue/Period)
намеренно развёрнуты в раздельные nullable-колонки, потому что семантика «не переопределено» требует
независимого null на уровне пары, а не пустого VO; парность обеспечивают check-констрейнты (INV-4).
Модели JSONB (versioned)
JSONB-модели не используются.
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
CatalogPropertyObjectCard | reference (логическая) | property_object_display_overrides.property_object_id → property_object_cards.id | пересчёт карточки объекта недвижимости | soft-delete оверрайда → пересчёт карточки из SourceSnapshot |
CatalogProjectCard | reference (логическая) | building_display_overrides.project_id → project_cards.id | пересчёт карточки проекта | то же |
CatalogPropertyObjectCard (объекты корпуса) | reference (логическая) | property_object_cards.building_id = building_display_overrides.building_id | пересчёт объектов корпуса | то же |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-1 SingleLiveOverride | На PropertyObjectId/BuildingId — не более одного не-удалённого оверрайда | unique partial index + проверка перед созданием | CATALOG_OVERRIDE_ALREADY_EXISTS |
| INV-2 TargetKnown | При создании цель (PropertyObjectId/BuildingId) существует в проекциях каталога; отсутствие → отказ (админ правит то, что видит в каталоге) | валидатор команды admin API | CATALOG_OVERRIDE_TARGET_NOT_FOUND |
| INV-3 AtLeastOneField | Хотя бы одно override-поле не null (пустой оверрайд бессмыслен) | проверка входа | CATALOG_OVERRIDE_EMPTY |
| INV-4 PairedFields | ReliabilityValue ⇔ ReliabilityMax; PaybackValue ⇔ PaybackPeriod (оба null или оба заданы) | check-констрейнты + валидатор | CATALOG_OVERRIDE_INCOMPLETE_PAIR |
| INV-5 PricePositive | Price = null или > 0 | check + валидатор | CATALOG_OVERRIDE_INVALID_PRICE |
| INV-6 RecomputeInTx | Любая мутация оверрайда пересчитывает эффективные поля затронутых карточек (объект; корпус → карточка проекта + все объекты корпуса) в той же транзакции | application handler (пересчёт в том же коммите, что и изменение оверрайда) | нет — механика |
Статусной машины нет (жизненный цикл: создан → изменён* → soft-deleted; выражен audit/soft-delete-полями, бизнес-статусов не несёт).
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
PropertyObjectDisplayOverrideChangedEvent | доменное | Create/Update/soft-delete оверрайда объекта | Id, PropertyObjectId, набор изменённых полей |
BuildingDisplayOverrideChangedEvent | доменное | Create/Update/soft-delete корпусного оверрайда | Id, BuildingId, ProjectId, набор изменённых полей |
Обработчики доменных событий выполняют пересчёт карточек (INV-6) в том же согласованном изменении. Интеграционные события не публикуются: оверрайд — внутренняя механика витрины, чужие контексты эффективные значения не потребляют (избранное/подборки читают карточки через API каталога).
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK pk_property_object_display_overrides, pk_building_display_overrides; ux_property_object_overrides_property_object_id unique btree (property_object_id) where deleted_at is null; ux_building_overrides_building_id unique btree (building_id) where deleted_at is null; ix_building_overrides_project_id btree; ix_property_object_overrides_created_at btree |
| Партиционирование / retention | none — единицы тысяч строк; история — бессрочно (soft-delete) |
| Concurrency | оптимистическая блокировка на admin-мутациях (правка одного администратора не затирает правку другого) |
| Permissions / PII | Только admin API (catalog/api/admin/*, m2m-авторизация + actor-id администратора — ADR-0053); permissions admin-каталога: catalog.overrides.view / catalog.overrides.manage. Comment — internal, в public-ответы не попадает. PII нет |
| Audit | Полный: отметки автора + soft-delete (правки витрины — чувствительная операция, расследуется службой заботы) |
Конкурентность и согласованность пересчёта
Пересчёт эффективных полей (INV-6) всегда берёт последний сохранённый SourceSnapshot карточки. Если между
применением оверрайда и пересчётом карточку успел обновить консюмер chessboard (пришёл новый
SourceSnapshot), оптимистическая блокировка карточки заставляет пересчёт повториться и переприменить
формулу Effective = Override ?? SourceSnapshot уже к свежему снапшоту: оверрайд не затирает структурное
обновление источника и наоборот.
Обработка PropertyObjectDisplayOverrideChangedEvent/BuildingDisplayOverrideChangedEvent детерминирована —
результат зависит только от текущего оверрайда и SourceSnapshot; повторный прогон по тем же входам даёт тот
же результат, поэтому повторная доставка доменного события карточку не искажает.
Корпусной оверрайд (IsVisible/ComplexName/Stars/…) пересчитывает карточку проекта и все объекты
корпуса в одной транзакции (INV-6): читатели видят либо полностью прежнее, либо полностью новое состояние
витрины корпуса, промежуточного среза нет.
INV-2 требует существования цели на входе; если карточка ушла в Delisted (источник удалил объект/корпус) до
пересчёта — пересчёт затрагивает 0 живых карточек, оверрайд сохраняется (INV-1 держит единственность живого
оверрайда) и «оживёт» при восстановлении объекта/корпуса в источнике.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: CatalogProjectCard, Entity / Aggregate: CatalogPropertyObjectCard
- API (6): Catalog Admin API — реестр методов и общие правила области, GET /api/admin/v1/buildings/{buildingId}/override — Просмотр оверрайда корпуса (admin), GET /api/admin/v1/lots/{propertyObjectId}/override — Просмотр оверрайда объекта недвижимости (admin), POST /api/admin/v1/projections/rebuild — Принудительный пересчёт карточек по id (admin), PUT /api/admin/v1/buildings/{buildingId}/override — Создание/изменение оверрайда корпуса (admin), PUT /api/admin/v1/lots/{propertyObjectId}/override — Создание/изменение оверрайда объекта недвижимости (admin)
- Use Cases (1): UC-CTL-007. Карточка объекта (ограниченная)
- Консюмеры (2): Consumer: Chessboard Projection Sync (pin-catalog-chessboard-sync), Consumer: Invest-Analytics Enrichment (pin-catalog-analytics-enrichment)
- Фоновые задачи (1): Background Job: DelistedRetentionJob
- index.md (1): Catalog Service (Pin.Catalog)
- registries (1): Реестр сущностей PIN
Связанные документы
- catalog-property-object-card.md /
catalog-project-card.md —
правило
Effective = Override ?? SourceSnapshot, пересчёты. - ADR-0053 (admin API), ADR-0056 (пересчёт в транзакции, без очередей).