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

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 typepin.chessboard.layout-group-upserted
C# typeLayoutGroupUpsertedKafkaEvent
Topicpin.chessboard.layout-group-upserted.v1
Partitions / key3 партиции (групп сотни на проект, низкочастотный агрегат) / key = Id — порядок per-group гарантирован
ContractVersion1
Producerchessboard: Developers API создания/обновления/архива группы, синк Talent (talent-chessboard-sync), background project-aggregates-recalc
AggregateTypeLayoutGroup
Статусdraft

Событие несёт метаданные: идентификатор сообщения (для дедупликации), время, идентификатор и тип агрегата-источника (AggregateId = Id, AggregateType = "LayoutGroup"); сквозной trace восстанавливается из метаданных. Публикуется атомарно со сменой состояния агрегата (транзакционный outbox).

Payload LayoutGroupUpsertedKafkaEvent

Snapshot публичного состояния группы. Дом полей — entity: LayoutGroupEntity; здесь перечислено только то, что реально в событии.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
IdGuidДаLayoutGroupEntity.IdImmutableКлюч агрегата (= Kafka key).
TenantIdGuidДаLayoutGroupEntity.TenantIdImmutableTenant boundary = DeveloperId проекта (ADR-0052).
ProjectIdGuidДаLayoutGroupEntity.ProjectIdImmutableПроект-владелец (ключ иерархии измерений dev-stats: …→ProjectId→LayoutGroupId→LayoutId→…).
NamestringДаLayoutGroupEntity.NameAdditive onlyНазвание группы («Студии», «Люкс с видом на море»); ≤ 128, уникально в проекте (INV-LGR-2).
SortOrderintДаLayoutGroupEntity.SortOrderAdditive onlyПорядок отображения групп в ассортименте проекта (0..1000).
LayoutsCountintДаLayoutGroupEntity.LayoutsCountAdditive onlyДенормализованный счётчик планировок группы (>= 0); пересчёт — background project-aggregates-recalc.
AreaNullableRange<decimal>НетLayoutGroupEntity.Area (area_min/area_max)Additive onlyДиапазон TotalArea планировок группы, м² (Area.MinArea.Max, area_max >= area_min); обе границы null — планировок в группе ещё нет.
IsArchivedboolДаLayoutGroupEntity.DeletedAt != nullAdditive onlyФлаг архива (soft-delete). true → потребители убирают группу из активных проекций (INV-LGR-3 — при живых планировках архив запрещён).
VersionlongДаLayoutGroupEntity.VersionImmutable 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-groupDeletedAt = 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). Архив доставляется как upsert IsArchived = 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.
  • Unknown enum — не применяется: enum-полей в payload нет (состояние группы — bool IsArchived, без статусной state-машины, layout-group.md).
  • Порядок per-group гарантирован key-ом (Id); потребители обязаны держать Version-guard (upsert создания, обновления, пересчёта и архива одного агрегата упорядочены между собой).
  • Deprecated-версия — sunset через 2 релиза после миграции последнего потребителя.

Безопасность

ПолеКласс данныхПравило
Id, TenantId, ProjectId, VersionInternalinclude
Name, SortOrder, LayoutsCount, Area, IsArchivedPublic (публичные атрибуты группы 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.

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

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

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