Entity / Aggregate: LayoutEntity
Дом полей агрегата «Планировка» контекста chessboard. Проекции (карточка объекта недвижимости в catalog, правила
вознаграждений rewards, эконом-модель по типам объектов недвижимости invest-analytics, измерение Layout в dev-stats)
ссылаются сюда и описывают только дельту.
Назначение
LayoutEntity — типовая планировка объекта недвижимости внутри проекта: схемы (2D/3D-план), типовые
площади, комнатность. Объекты недвижимости ссылаются на планировку (PropertyObject.LayoutId); планировка
опционально входит в группу планировок (layout-group.md). На планировках строятся
правила вознаграждений (rewards) и типизация объектов эконом-модели (invest-analytics), а dev-stats
использует её как уровень фильтрации статистики (ADR-0057).
Классификация
| Поле | Значение |
|---|---|
| Контекст | chessboard |
| Kind | AggregateRoot (гибрид с репликой Talent) |
| Source of truth | chessboard (PIN-поля) / talent.microservices.chessboard (реплика-поля) |
| Таблица | chessboard.layouts |
| Tenant isolation | TenantId = DeveloperId проекта (ADR-0052); публичное чтение — для планировок published-проектов |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app / Talent sync | Идентификатор планировки. |
TenantId | Guid | Да | index ix_layouts_tenant_id | app | Tenant boundary (ADR-0052). |
ProjectId | Guid | Да | FK fk_layouts_project → chessboard.projects.id, index | app | Проект-владелец (планировки типовые в рамках проекта). |
LayoutGroupId | Guid? | Нет | FK fk_layouts_layout_group → chessboard.layout_groups.id, index where not null | app / Talent sync | Группа планировок (INV-LAY-3: группа того же проекта). |
Name | string | Да | varchar(128), min 1; unique ux_layouts_project_name (project_id, lower(name)) where deleted_at is null | Talent sync / app | Название: «1К-А 32.5», «Студия S». Уникально в проекте (INV-LAY-2). |
Rooms | short | Да | check rooms between 0 and 20; 0 = студия | Talent sync / app | Типовая комнатность планировки. |
TotalArea | decimal | Да | numeric(8,2), check total_area > 0 | Talent sync / app | Типовая общая площадь, м². |
LivingArea | decimal? | Нет | numeric(8,2), check living_area > 0 and living_area <= total_area | Talent sync / app | Типовая жилая площадь, м². |
Schemas | LayoutSchemasDocument (jsonb) | Да (дефолт пустой) | jsonb, typed | Talent sync / app | Схемы планировки (см. JSONB). |
Description | string? | Нет | varchar(2000) | app | Описание планировки (PIN-поле). |
PropertyObjectsCount | int | Да | >= 0, default 0 | вычисляется (background project-aggregates-recalc) | Денормализованный счётчик объектов недвижимости с этой планировкой. |
ExternalTalentLayoutId | Guid? | Нет | unique ux_layouts_external_talent_layout_id (where not null) | Talent sync | Id планировки в Talent Chessboard. |
SyncSource | ChessboardSyncSource | Да | enum (текст), varchar(32) | app / consumer | Pin / TalentChessboard (реплика-поля: Name, Rooms, площади, Schemas, LayoutGroupId-привязка). |
ReplicaVersion | long? | Нет (обязателен при реплике) | >= 0 | Talent sync | Anti-out-of-order guard (INV-LAY-4). |
LastSyncedAt | DateTimeOffset? | Нет | — | Talent sync | Последний применённый синк. |
CreatedAt | DateTimeOffset | Да | index ix_layouts_created_at | app | Дата создания. |
CreatedByUserId | Guid? | Нет | — | app | Кто создал. |
UpdatedAt | DateTimeOffset | Да | — | app | Дата изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | partial-index filter | app | Soft-delete (архив планировки). |
DeletedByUserId | Guid? | Нет | — | app | Кто архивировал. |
Version | long | Да | >= 1 | app | Версия агрегата. |
Модели JSONB (versioned)
LayoutSchemasDocument (jsonb, колонка schemas, ContractVersion = 1)
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
ContractVersion | int | Да | = 1 | v1 |
Items | IReadOnlyList<LayoutSchemaModel> | Да (может быть пустым) | ≤ 20 элементов; сортировка по SortOrder | v1 |
Вложенная модель LayoutSchemaModel
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|---|---|---|---|
FileId | Guid | Да | внешний storage/CDN | v1 |
Kind | LayoutSchemaKind (enum-string) | Да | Plan2D / Plan3D / FloorPosition (положение на этаже) / Visualization | v1 |
Title | string? | Нет | varchar(128) | v1 |
SortOrder | int | Да | 0..100 | v1 |
Unknown Kind из синка Talent маппится в Plan2D c warning-логом.
Связи
| Связь | Тип | Ключ | Delete behavior |
|---|---|---|---|
ProjectEntity | reference (родитель) | layouts.project_id → projects.id | restrict |
LayoutGroupEntity | reference | layouts.layout_group_id → layout_groups.id | restrict (группа не архивируется при живых планировках) |
PropertyObjectEntity | reference (обратная) | property_objects.layout_id → layouts.id | restrict — архив планировки запрещён при не удалённых объектах недвижимости (INV-LAY-5) |
Инварианты и переходы состояний
| Инвариант | Условие | Когда проверяется | Ошибка |
|---|---|---|---|
| INV-LAY-1 TenantFromProject | TenantId == Project.TenantId | при создании | CHESSBOARD_LAYOUT_TENANT_MISMATCH |
| INV-LAY-2 UniqueNameInProject | Name уникально среди не удалённых планировок проекта | при создании и переименовании | CHESSBOARD_LAYOUT_NAME_TAKEN |
| INV-LAY-3 GroupSameProject | LayoutGroup.ProjectId == ProjectId при привязке | при привязке к группе | CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH |
| INV-LAY-4 MonotonicReplica | Событие синка — только при event.Version > ReplicaVersion | при применении события синка | идемпотентный пропуск |
| INV-LAY-5 ArchiveGuard | Архив (DeletedAt) запрещён при существующих не удалённых объектах недвижимости с LayoutId = Id | при архивировании | CHESSBOARD_LAYOUT_HAS_PROPERTY_OBJECTS |
| INV-LAY-6 SyncOwnedFields | Реплика-поля меняются только синком; PIN-поля — только локально | при изменении полей (раздельные пути) | CHESSBOARD_LAYOUT_FIELD_SYNC_MANAGED |
Статусной state-машины нет: создана → архивирована через soft-delete.
Жизненный цикл и операции
| Операция | Актор / источник | Предусловие (guard) | Эффект | Отказ |
|---|---|---|---|---|
| Create | синк Talent / застройщик | TenantId = Project.TenantId (INV-LAY-1); имя свободно в проекте (INV-LAY-2); привязка к группе того же проекта (INV-LAY-3); TotalArea > 0; LivingArea <= TotalArea | создана планировка, Version = 1, PropertyObjectsCount = 0 | CHESSBOARD_LAYOUT_TENANT_MISMATCH / CHESSBOARD_LAYOUT_NAME_TAKEN / CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH |
| Rename | синк (реплика-поле) | новое имя свободно в проекте | Name обновлён, Version += 1 | CHESSBOARD_LAYOUT_NAME_TAKEN (при синке — суффикс (Talent) + warn, событие не теряется) |
| Reassign group | синк / застройщик | группа того же проекта (INV-LAY-3) | LayoutGroupId обновлён; пересчёт LayoutsCount/Area затронутых групп — джоб recalc | CHESSBOARD_LAYOUT_GROUP_PROJECT_MISMATCH |
| Update schemas / rooms / площади | синк (реплика-поля) | TotalArea > 0, LivingArea <= TotalArea, ≤ 20 схем | поля обновлены, Version += 1 | значения вне диапазона синк корректирует (null/fallback + warn) |
| Update description (PIN) | застройщик | — | Description обновлён (синком не перезаписывается, INV-LAY-6) | — |
| Archive (soft-delete) | застройщик | нет не удалённых объектов недвижимости с LayoutId = Id (INV-LAY-5, FK restrict) | DeletedAt проставлен; исчезает из public-проекций | CHESSBOARD_LAYOUT_HAS_PROPERTY_OBJECTS |
| Restore (возврат из архива) | застройщик | имя снова свободно в проекте | DeletedAt = null | CHESSBOARD_LAYOUT_NAME_TAKEN |
Постусловие мутаций: Version монотонно растёт; значимое изменение переопубликуется LayoutUpsertedKafkaEvent;
PropertyObjectsCount/групповые Area/LayoutsCount — денормализация, обновляется джобом recalc, а не в этой
операции.
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
LayoutCreatedEvent | доменное | Успешное создание | Id, ProjectId |
LayoutUpsertedKafkaEvent | интеграционное (ContractVersion = 1, топик pin.chessboard.layout-upserted.v1, key = Id) | Создание, обновление, архив (IsArchived = true) | Snapshot: Id, TenantId, ProjectId, LayoutGroupId, Name, Rooms, TotalArea, LivingArea, PropertyObjectsCount, IsArchived, Version. Потребители: dev-stats (измерение Layout), rewards (условия правил по Layout), invest-analytics (типы объектов недвижимости), catalog |
Интеграционные события публикуются атомарно со сменой состояния агрегата.
Кросс-контекстный контракт (LayoutUpsertedKafkaEvent)
- Ключ / версия. key =
Id,ContractVersion = 1, монотонныйVersion; upsert-by-id, at-least-once. - Кто ожидает.
dev-stats(измерение Layout),rewards(условия правил вознаграждений по Layout),invest-analytics(типизация объектов эконом-модели),catalog(карточка объекта недвижимости). PropertyObjectsCount— производное. Считает джобproject-aggregates-recalcи переопубликует событие при изменении счётчика; downstream берут его из snapshot.- Архив. Архив планировки идёт снапшотом с
IsArchived = true— потребители помечают запись архивной, а не удаляют (историческая привязка объектов/правил сохраняется).
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK pk_layouts; ux_layouts_project_name unique (project_id, lower(name)) where deleted_at is null; ux_layouts_external_talent_layout_id unique where not null; ix_layouts_tenant_id; ix_layouts_project_id; ix_layouts_layout_group_id where not null; ix_layouts_created_at |
| Партиционирование / retention | none — планировок тысячи; retention бессрочный (soft-delete) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую); для событий синка — guard по ReplicaVersion |
| Permissions / PII | Public-чтение — планировки published-проектов (схемы видны всем в карточке объекта недвижимости); CRUD — chessboard.layouts.manage (scope застройщика); PII нет |
| Audit | версия агрегата + отметки кто/когда; события |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (4): Entity / Aggregate: LayoutGroupEntity, Entity / Aggregate: ProjectEntity, Entity / Aggregate: PropertyObjectEntity, Enum-справочники контекста chessboard
- API (14): Chessboard API — реестр операций областей Developers и Admin, GET /api/v1/developers/projects/{projectId}/layouts — Список планировок проекта, GET /api/v1/developers/projects/{projectId}/property-objects — Список объектов недвижимости проекта, GET /api/v1/developers/property-objects/{propertyObjectId} — Карточка объекта недвижимости (кабинет застройщика), GET /api/v1/public/property-objects — Список объектов недвижимости проекта/здания с фильтрами, GET /api/v1/public/property-objects/{propertyObjectId} — Карточка объекта/объекта недвижимости (ограниченная/полная), POST /api/v1/developers/buildings/{buildingId}/property-objects — Создание объекта недвижимости, POST /api/v1/developers/buildings/{buildingId}/property-objects/bulk — Массовый upsert объектов недвижимости, POST /api/v1/developers/layout-groups/{layoutGroupId}/archive — Архивирование группы планировок, POST /api/v1/developers/layouts/{layoutId}/archive — Архивирование планировки, POST /api/v1/developers/projects/{projectId}/layouts — Создание планировки, POST /api/v1/internal/property-objects/by-ids — Батч-выдача объектов недвижимости по идентификаторам (internal, для catalog/crm) (+2)
- Use Cases (2): UC-CHS-002. Добавление здания (литера/корпуса) и объектов проекта застройщиком, UC-CHS-005. Синхронизация реплики из Talent Chessboard (system)
- События (1): Event Contract: pin.chessboard.layout-upserted (LayoutUpsertedKafkaEvent)
- Консюмеры (2): Consumer: Chessboard Dimensions Sync (pin-rewards-chessboard-dimensions), Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync)
- Фоновые задачи (2): Background Job: project-aggregates-recalc (пересчёт денормализованных агрегатов), Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Chessboard)
- index.md (1): Chessboard Service (Pin.Chessboard)
- dimensions (1): dim_layout (ClickHouse dimension)
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- layout-group.md — группа; property-object.md — объекты недвижимости планировки;
project.md — родитель; reference-data.md —
ChessboardSyncSource. - Потребители снапшотов:
docs/06-services/rewards/(правила по Layout/LayoutGroup),docs/06-services/dev-stats/(ADR-0057),docs/06-services/invest-analytics/.