Entity / Aggregate: LayoutGroupEntity
Дом полей агрегата «Группа планировок» контекста chessboard. Проекции (правила вознаграждений rewards,
уровень фильтрации LayoutGroup в dev-stats (ADR-0057), группировка в карточке проекта catalog)
ссылаются сюда и описывают только дельту.
Назначение
LayoutGroupEntity — именованная группа типовых планировок проекта: «Студии», «1-комнатные», «Люксы
с видом на море». Группа — уровень иерархии между проектом и планировкой: на неё настраиваются правила
вознаграждений (rewards), по ней агрегируется статистика просмотров (dev-stats), она же — укрупнённый
«тип объекта недвижимости» для отображения ассортимента.
Классификация
| Поле | Значение |
|---|---|
| Контекст | chessboard |
| Kind | AggregateRoot (гибрид с репликой Talent; справочная сущность) |
| Source of truth | chessboard (PIN-поля) / talent.microservices.chessboard (реплика-поля) |
| Таблица | chessboard.layout_groups |
| Tenant isolation | TenantId = DeveloperId проекта (ADR-0052) |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app / Talent sync | Идентификатор группы. |
TenantId | Guid | Да | index ix_layout_groups_tenant_id | app | Tenant boundary (ADR-0052). |
ProjectId | Guid | Да | FK fk_layout_groups_project → chessboard.projects.id, index | app | Проект-владелец. |
Name | string | Да | varchar(128), min 1; unique ux_layout_groups_project_name (project_id, lower(name)) where deleted_at is null | Talent sync / app | Название группы («Студии», «Люкс с видом на море»). Уникально в проекте (INV-LGR-2). |
Description | string? | Нет | varchar(2000) | app | Описание группы (PIN-поле). |
SortOrder | int | Да | 0..1000, default 0 | app | Порядок отображения групп в ассортименте проекта. |
LayoutsCount | int | Да | >= 0, default 0 | вычисляется (background project-aggregates-recalc) | Денормализованный счётчик планировок группы. |
Area | NullableRange<decimal> | Нет | колонки area_min / area_max, numeric(8,2), area_max >= area_min | вычисляется (background) | Диапазон TotalArea планировок группы (Area.Min … Area.Max, «от … м²» в ассортименте); обе границы null, если планировок в группе ещё нет. |
ExternalTalentLayoutGroupId | Guid? | Нет | unique ux_layout_groups_external_talent_id (where not null) | Talent sync | Id группы в Talent Chessboard. |
SyncSource | ChessboardSyncSource | Да | enum (текст), varchar(32) | app / consumer | Pin / TalentChessboard (реплика-поле: Name). |
ReplicaVersion | long? | Нет (обязателен при реплике) | >= 0 | Talent sync | Anti-out-of-order guard (INV-LGR-4). |
LastSyncedAt | DateTimeOffset? | Нет | — | Talent sync | Последний применённый синк. |
CreatedAt | DateTimeOffset | Да | index ix_layout_groups_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 | Версия агрегата. |
Value objects
Area — NullableRange<decimal>
Диапазон площадей планировок группы, хранится двумя границами area_min / area_max (обе nullable).
Инвариант area_max >= area_min при обоих заполненных значениях; значения вычисляются фоновым процессом
project-aggregates-recalc. Остальные поля группы атомарны.
Связи
| Связь | Тип | Ключ | Delete behavior |
|---|---|---|---|
ProjectEntity | reference (родитель) | layout_groups.project_id → projects.id | restrict |
LayoutEntity | reference (обратная) | layouts.layout_group_id → layout_groups.id | restrict — архив группы запрещён при не архивированных планировках (INV-LGR-3) |
Инварианты и переходы состояний
| Инвариант | Условие | Когда проверяется | Ошибка |
|---|---|---|---|
| INV-LGR-1 TenantFromProject | TenantId == Project.TenantId | при создании | CHESSBOARD_LAYOUT_GROUP_TENANT_MISMATCH |
| INV-LGR-2 UniqueNameInProject | Name уникально среди не удалённых групп проекта | при создании и переименовании | CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN |
| INV-LGR-3 ArchiveGuard | Архив запрещён при существующих не архивированных планировках группы | при архивировании | CHESSBOARD_LAYOUT_GROUP_HAS_LAYOUTS |
| INV-LGR-4 MonotonicReplica | Событие синка — только при event.Version > ReplicaVersion | при применении события синка | идемпотентный пропуск |
Статусной state-машины нет: создана → архивирована через soft-delete.
Жизненный цикл и операции
| Операция | Актор / источник | Предусловие (guard) | Эффект | Отказ |
|---|---|---|---|---|
| Create | синк Talent / застройщик | TenantId = Project.TenantId (INV-LGR-1); имя свободно в проекте (INV-LGR-2) | создана группа, Version = 1, LayoutsCount = 0, Area обе границы null | CHESSBOARD_LAYOUT_GROUP_TENANT_MISMATCH / CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN |
| Rename | синк (реплика-поле Name) | новое имя свободно в проекте | Name обновлён, Version += 1; при синке гонка версий отсекается ReplicaVersion-guard (INV-LGR-4) | CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN |
| Update description / sort order (PIN) | застройщик | SortOrder в 0..1000 | поля обновлены, Version += 1 (синком не перезаписываются) | — |
| Archive (soft-delete) | застройщик | нет не архивированных планировок группы (INV-LGR-3, FK restrict) | DeletedAt проставлен; снапшот IsArchived = true | CHESSBOARD_LAYOUT_GROUP_HAS_LAYOUTS |
| Restore (возврат из архива) | застройщик | имя снова свободно в проекте | DeletedAt = null | CHESSBOARD_LAYOUT_GROUP_NAME_TAKEN |
Постусловие мутаций: Version монотонно растёт; значимое изменение переопубликуется
LayoutGroupUpsertedKafkaEvent; LayoutsCount/Area — денормализация, вычисляет джоб recalc (по составу
планировок группы), а не операция над группой.
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
LayoutGroupCreatedEvent | доменное | Успешное создание | Id, ProjectId |
LayoutGroupUpsertedKafkaEvent | интеграционное (ContractVersion = 1, топик pin.chessboard.layout-group-upserted.v1, key = Id) | Создание, обновление, архив (IsArchived = true) | Snapshot: Id, TenantId, ProjectId, Name, SortOrder, LayoutsCount, Area (NullableRange<decimal>), IsArchived, Version. Потребители: dev-stats (измерение LayoutGroup, ADR-0057), rewards (условия правил по LayoutGroup), catalog |
Интеграционные события публикуются атомарно со сменой состояния агрегата.
Кросс-контекстный контракт (LayoutGroupUpsertedKafkaEvent)
- Ключ / версия. key =
Id,ContractVersion = 1, монотонныйVersion; upsert-by-id, at-least-once. - Кто ожидает.
dev-stats(измерение LayoutGroup, ADR-0057),rewards(условия правил вознаграждений по LayoutGroup),catalog(группировка ассортимента в карточке проекта). LayoutsCount/Area— производные. Пересчитывает джобproject-aggregates-recalcпо составу планировок группы и переопубликует событие при их изменении; downstream берут из snapshot.- Архив. Идёт снапшотом с
IsArchived = true— потребители помечают запись архивной, не удаляя (условия правилrewards/измеренияdev-stats, ссылающиеся на группу, остаются историчны).
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK pk_layout_groups; ux_layout_groups_project_name unique (project_id, lower(name)) where deleted_at is null; ux_layout_groups_external_talent_id unique where not null; ix_layout_groups_tenant_id; ix_layout_groups_project_id; ix_layout_groups_created_at |
| Партиционирование / retention | none — групп сотни; retention бессрочный (soft-delete) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую); для событий синка — guard по ReplicaVersion |
| Permissions / PII | Public-чтение — группы published-проектов; CRUD — chessboard.layouts.manage (scope застройщика, общий с планировками); PII нет |
| Audit | версия агрегата + отметки кто/когда; события |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Entity / Aggregate: LayoutEntity, Entity / Aggregate: ProjectEntity, Enum-справочники контекста chessboard
- API (9): Chessboard API — реестр операций областей Developers и Admin, GET /api/v1/developers/projects/{projectId}/layout-groups — Список групп планировок, GET /api/v1/developers/projects/{projectId}/layouts — Список планировок проекта, GET /api/v1/public/property-objects — Список объектов недвижимости проекта/здания с фильтрами, POST /api/v1/developers/layout-groups/{layoutGroupId}/archive — Архивирование группы планировок, POST /api/v1/developers/projects/{projectId}/layout-groups — Создание группы планировок, POST /api/v1/developers/projects/{projectId}/layouts — Создание планировки, PUT /api/v1/developers/layout-groups/{layoutGroupId} — Обновление группы планировок, PUT /api/v1/developers/layouts/{layoutId} — Обновление планировки
- Use Cases (2): UC-CHS-002. Добавление здания (литера/корпуса) и объектов проекта застройщиком, UC-CHS-005. Синхронизация реплики из Talent Chessboard (system)
- События (1): Event Contract: pin.chessboard.layout-group-upserted (LayoutGroupUpsertedKafkaEvent)
- Консюмеры (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_group (ClickHouse dimension)
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- layout.md — планировки группы; project.md — родитель;
reference-data.md —
ChessboardSyncSource. - Потребители:
docs/06-services/rewards/,docs/06-services/dev-stats/(ADR-0057).