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

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)ИсточникОписание
IdGuidДаPKappИдентификатор оверрайда.
PropertyObjectIdGuidДа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).
IsVisiblebool?Нетadminfalse — скрыть объект из витрины; true — принудительно показать (перекрывает только витринную видимость, не PropertyObjectStatus=Hidden источника — INV-1 property-object-card требует оба условия); null — не переопределено.
Pricedecimal(14,2)?Нет> 0 (check ck_property_object_overrides_price)adminВитринная цена вместо цены источника.
Titlestring?Нетvarchar(512), min 1 после trimadminЗаголовок карточки.
Badgestring?Нетvarchar(64)adminМаркетинговый бейдж.
ReliabilityValueshort?Нет0..ReliabilityMax; пара с ReliabilityMax (check: оба null или оба заданы)adminЗначение индекса надёжности.
ReliabilityMaxshort?Нет1..100adminМаксимум шкалы надёжности.
PaybackValuedecimal(5,1)?Нет> 0; пара с PaybackPeriod (check)adminЗначение окупаемости.
PaybackPeriodPaybackPeriod? (enum-string)НетYears/Months, varchar(16) (enum — дом catalog-property-object-card.md)adminЕдиница окупаемости.
Ratingdecimal(3,1)?Нет0.0..10.0adminРейтинг карточки.
Commentstring?Нетvarchar(1024)adminСлужебный комментарий администратора «зачем правка» (не отдаётся в public API).
CreatedAtDateTimeOffsetДаindex ix_property_object_overrides_created_atapp
CreatedByUserIdGuid?Да (для admin-мутаций)без FK (admin вне identity, ADR-0053)appАвтор правки.
UpdatedAtDateTimeOffsetДаapp
UpdatedByUserIdGuid?Нетapp
DeletedAtDateTimeOffset?Нетpartial-фильтр рабочих индексовappSoft-delete = «оверрайд снят».
DeletedByUserIdGuid?Нетapp

Поля BuildingDisplayOverride

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор оверрайда.
BuildingIdGuidДаunique ux_building_overrides_building_id where deleted_at is nulladmin APIКорпус chessboard. Один живой оверрайд на корпус (INV-1).
ProjectIdGuidДаindex ix_building_overrides_project_id; логическая ссылка на catalog.project_cards.idadmin API (резолвится из карточки корпуса при создании)Денормализованный проект — пересчёт карточки проекта без обратного lookup-а.
IsVisiblebool?Нетadminfalse — скрыть корпус: все объекты корпуса → Hidden (INV-1 property-object-card).
ComplexNamestring?Нетvarchar(256), min 1 после trimadminНазвание комплекса — эффективное Name карточки проекта.
Starsshort?Нет1..5adminЗвёздность отеля.
DeliveryDateDateOnly?НетadminСрок сдачи.
ReadinessPercentshort?Нет0..100adminГотовность строительства.
Addressstring?Нетvarchar(512)adminПравка адреса витрины.
DistanceMinutesint?Нет>= 0adminМинуты до ключевой точки.
Commentstring?Нетvarchar(1024)adminСлужебный комментарий.
CreatedAt / CreatedByUserId / UpdatedAt / UpdatedByUserId / DeletedAt / DeletedByUserIdкак у PropertyObjectDisplayOverrideappAudit/soft-delete — идентично.

Value objects

Отдельных value objects нет. Пары «значение+шкала» (ReliabilityValue/Max, PaybackValue/Period) намеренно развёрнуты в раздельные nullable-колонки, потому что семантика «не переопределено» требует независимого null на уровне пары, а не пустого VO; парность обеспечивают check-констрейнты (INV-4).

Модели JSONB (versioned)

JSONB-модели не используются.

Связи

СвязьТипКлючПравило загрузкиDelete behavior
CatalogPropertyObjectCardreference (логическая)property_object_display_overrides.property_object_id → property_object_cards.idпересчёт карточки объекта недвижимостиsoft-delete оверрайда → пересчёт карточки из SourceSnapshot
CatalogProjectCardreference (логическая)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 APICATALOG_OVERRIDE_TARGET_NOT_FOUND
INV-3 AtLeastOneFieldХотя бы одно override-поле не null (пустой оверрайд бессмыслен)проверка входаCATALOG_OVERRIDE_EMPTY
INV-4 PairedFieldsReliabilityValueReliabilityMax; PaybackValuePaybackPeriod (оба null или оба заданы)check-констрейнты + валидаторCATALOG_OVERRIDE_INCOMPLETE_PAIR
INV-5 PricePositivePrice = null или > 0check + валидатор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
Партиционирование / retentionnone — единицы тысяч строк; история — бессрочно (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 (правки витрины — чувствительная операция, расследуется службой заботы)

Конкурентность и согласованность пересчёта

Оверрайд vs источник

Пересчёт эффективных полей (INV-6) всегда берёт последний сохранённый SourceSnapshot карточки. Если между применением оверрайда и пересчётом карточку успел обновить консюмер chessboard (пришёл новый SourceSnapshot), оптимистическая блокировка карточки заставляет пересчёт повториться и переприменить формулу Effective = Override ?? SourceSnapshot уже к свежему снапшоту: оверрайд не затирает структурное обновление источника и наоборот.

Идемпотентность пересчёта

Обработка PropertyObjectDisplayOverrideChangedEvent/BuildingDisplayOverrideChangedEvent детерминирована — результат зависит только от текущего оверрайда и SourceSnapshot; повторный прогон по тем же входам даёт тот же результат, поэтому повторная доставка доменного события карточку не искажает.

Множество затронутых карточек

Корпусной оверрайд (IsVisible/ComplexName/Stars/…) пересчитывает карточку проекта и все объекты корпуса в одной транзакции (INV-6): читатели видят либо полностью прежнее, либо полностью новое состояние витрины корпуса, промежуточного среза нет.

Цель делистнута между проверкой и применением

INV-2 требует существования цели на входе; если карточка ушла в Delisted (источник удалил объект/корпус) до пересчёта — пересчёт затрагивает 0 живых карточек, оверрайд сохраняется (INV-1 держит единственность живого оверрайда) и «оживёт» при восстановлении объекта/корпуса в источнике.

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

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

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