Event Contract: pin.chessboard.layout-group-upserted (LayoutGroupUpsertedKafkaEvent)
Версионируемый контракт интеграционного события «группа планировок создана/обновлена/архивирована».
Consumer-документы ссылаются сюда, не переописывая payload. Это единственное интеграционное событие агрегата
LayoutGroupEntity: снапшот укрупнённого «типа объекта недвижимости» проекта; архив выражается флагом
IsArchived = true в том же snapshot-е (отдельного delete-события нет — группа архивируется через
soft-delete).
Назначение
Фиксирует создание LayoutGroupEntity (синк Talent, Developers API), обновление её публичных полей
(Name / SortOrder), пересчёт денормализаций (LayoutsCount / Area — background
project-aggregates-recalc) и архивирование (IsArchived = true). Событие публичное: даёт
downstream-контекстам snapshot группы без синхронного запроса в chessboard — уровень фильтрации/измерения
LayoutGroup статистики застройщика (ADR-0057), условия правил вознаграждений по группе, группировка
ассортимента в карточке проекта витрины.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.chessboard.layout-group-upserted |
| C# type | LayoutGroupUpsertedKafkaEvent |
| Topic | pin.chessboard.layout-group-upserted.v1 |
| Partitions / key | 3 партиции (групп сотни на проект, низкочастотный агрегат) / key = Id — порядок per-group гарантирован |
| ContractVersion | 1 |
| Producer | chessboard: Developers API создания/обновления/архива группы, синк Talent (talent-chessboard-sync), background project-aggregates-recalc |
| AggregateType | LayoutGroup |
| Статус | draft |
Событие несёт метаданные: идентификатор сообщения (для дедупликации), время, идентификатор и тип
агрегата-источника (AggregateId = Id, AggregateType = "LayoutGroup"); сквозной trace восстанавливается
из метаданных. Публикуется атомарно со сменой состояния агрегата (транзакционный outbox).
Payload LayoutGroupUpsertedKafkaEvent
Snapshot публичного состояния группы. Дом полей — entity: LayoutGroupEntity;
здесь перечислено только то, что реально в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | LayoutGroupEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
TenantId | Guid | Да | LayoutGroupEntity.TenantId | Immutable | Tenant boundary = DeveloperId проекта (ADR-0052). |
ProjectId | Guid | Да | LayoutGroupEntity.ProjectId | Immutable | Проект-владелец (ключ иерархии измерений dev-stats: …→ProjectId→LayoutGroupId→LayoutId→…). |
Name | string | Да | LayoutGroupEntity.Name | Additive only | Название группы («Студии», «Люкс с видом на море»); ≤ 128, уникально в проекте (INV-LGR-2). |
SortOrder | int | Да | LayoutGroupEntity.SortOrder | Additive only | Порядок отображения групп в ассортименте проекта (0..1000). |
LayoutsCount | int | Да | LayoutGroupEntity.LayoutsCount | Additive only | Денормализованный счётчик планировок группы (>= 0); пересчёт — background project-aggregates-recalc. |
Area | NullableRange<decimal> | Нет | LayoutGroupEntity.Area (area_min/area_max) | Additive only | Диапазон TotalArea планировок группы, м² (Area.Min … Area.Max, area_max >= area_min); обе границы null — планировок в группе ещё нет. |
IsArchived | bool | Да | LayoutGroupEntity.DeletedAt != null | Additive only | Флаг архива (soft-delete). true → потребители убирают группу из активных проекций (INV-LGR-3 — при живых планировках архив запрещён). |
Version | long | Да | LayoutGroupEntity.Version | Immutable semantics | Доменная версия агрегата; монотонный guard у потребителей (событие с меньшей/равной версией — skip). Version = 1 ⇔ создание группы. |
Кроме Area (NullableRange<decimal>, две границы area_min/area_max, layout-group.md) вложенных
моделей нет. Description, ExternalTalentLayoutGroupId, SyncSource, ReplicaVersion в payload не входят:
описание — внутреннее PIN-поле, не нужное потребителям; реплика-метаданные синка — служебные (граница
идемпотентности consumer-а синка, не публичный контракт).
Когда публикуется
| Условие | Переход состояния | Согласованность |
|---|---|---|
| Создание группы: Developers API create-layout-group или синк Talent | [*] → активна (IsArchived = false, Version = 1) | публикация атомарна со сменой состояния агрегата |
Обновление публичных полей: Name / SortOrder (Developers API update-layout-group) или Name реплика-поле (синк Talent) | без перехода | та же |
Пересчёт денормализаций: LayoutsCount / Area (Min/Max) (background project-aggregates-recalc при появлении/архиве планировок группы) | без перехода (bump Version) | та же |
Архив: Developers API archive-layout-group — DeletedAt = now, guard INV-LGR-3 | активна → архив (IsArchived = true) | та же |
Отдельного delete-события нет — архив доставляется как upsert с IsArchived = true (потребители
перезаписывают свою реплику и деактивируют группу).
Потребители (consumers)
| Consumer-контекст | Эффект (событие → что меняется) | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
dev-stats | Реплика измерения LayoutGroup (уровень иерархии обогащения view-facts, ADR-0057); IsArchived = true → пометка измерения архивным | MessageId + guard по Version | < 5 мин |
rewards | Справочник групп для условий правил вознаграждений по LayoutGroup (привязка/отображение Name); IsArchived = true → правила по группе неактивны | то же | < 5 мин |
catalog | Группировка ассортимента в карточке проекта витрины (Name, SortOrder, Area (Min/Max), LayoutsCount); IsArchived = true → скрытие группы | то же | < 5 мин |
Доставка и обработка отказов
- Доставка — at-least-once. Транзакционный outbox гарантирует, что событие не теряется при коммите
агрегата, но допускает повторную доставку; порядок сообщений по ключу (
Id) сохраняется в пределах партиции. - Идемпотентность потребителя. Дубликат отсеивается по идентификатору сообщения; переупорядоченное или
устаревшее событие отбрасывается монотонным
Version-guard-ом (событие с версией ≤ применённой — skip). Архив доставляется как upsertIsArchived = true— повтор архива идемпотентен (реплика уже архивна). - Ретраи. Временная ошибка потребителя → повтор с backoff до успеха; событие не теряется, порядок per-group не нарушается.
- DLQ. Стабильно необрабатываемое сообщение после исчерпания ретраев уходит в DLQ потребителя с алертом —
чтобы не блокировать партицию; расхождение реплики закрывается повторной обработкой из DLQ либо ресинком
измерения из источника (
chessboard). - Окно согласованности. Пока потребитель не догнал (SLA из таблицы «Потребители»), группировка
ассортимента витрины и условия правил по
LayoutGroupвидят прежнее состояние группы; пересчётLayoutsCount/Area(background) отражается у потребителей с задержкой в пределах SLA.
Версионирование и совместимость
- Версия — один
ContractVersion = 1; при несовпадении версии события — warn-and-proceed. Топик кодирует версию конвенцией суффикса.v1. - Additive-only для minor: новые поля — optional с безопасным default, без bump.
- Breaking (удаление/переименование/смена типа/смысла поля) — новый
ContractVersion = 2+ новый топикpin.chessboard.layout-group-upserted.v2+ dual-publish на период миграции + deadline миграции consumers- sunset
.v1.
- sunset
- Unknown enum — не применяется: enum-полей в payload нет (состояние группы —
bool IsArchived, без статусной state-машины, layout-group.md). - Порядок per-group гарантирован key-ом (
Id); потребители обязаны держатьVersion-guard (upsert создания, обновления, пересчёта и архива одного агрегата упорядочены между собой). - Deprecated-версия — sunset через 2 релиза после миграции последнего потребителя.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
Id, TenantId, ProjectId, Version | Internal | include |
Name, SortOrder, LayoutsCount, Area, IsArchived | Public (публичные атрибуты группы published-проекта) | include |
PII в событии нет (группа планировок — справочная сущность без персональных данных, layout-group.md),
поэтому маскирование payload не требуется. В логах фигурируют только идентификаторы (Id/ProjectId/TenantId)
— не чувствительны. Событие НЕ содержит secrets, tokens, raw credentials, платёжных данных, unbounded
вложений (схемы планировок — событие layout-upserted). Доступ к топику — m2m ACL Kafka
(dev-stats/rewards/catalog); tenant-изоляция обеспечивается наличием TenantId в payload.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): POST /api/v1/developers/layout-groups/{layoutGroupId}/archive — Архивирование группы планировок, POST /api/v1/developers/projects/{projectId}/layout-groups — Создание группы планировок
- События (1): Event Contract: pin.chessboard.layout-upserted (LayoutUpsertedKafkaEvent)
- Консюмеры (2): Consumer: DevStatsLayoutGroupUpsertedConsumer, Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync)
- Фоновые задачи (2): Background Job: project-aggregates-recalc (пересчёт денормализованных агрегатов), Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Chessboard)
- dimensions (1): dim_layout_group (ClickHouse dimension)
- registries (1): Реестр событий PIN
Связанные документы
- Источники (операции): create-layout-group.md,
update-layout-group.md,
archive-layout-group.md; синк —
talent-chessboard-sync.md; пересчёт денормализаций — background
project-aggregates-recalc. - Сущность: layout-group.md (INV-LGR-1…4, состав события); родитель: project.md; соседнее событие агрегата планировок: layout-upserted.md.
- ADR-0052 (tenancy), ADR-0057 (dev-stats потребляет измерения).