Event Contract: pin.chessboard.project-upserted (ProjectUpsertedKafkaEvent)
Версионируемый контракт интеграционного события «проект создан/обновлён». Consumer-документы ссылаются сюда,
не переописывая payload. Это единственное snapshot-событие агрегата Project: в отличие от объекта
недвижимости (где статус и цена вынесены в отдельные высокочастотные топики), изменения проекта редки,
поэтому один upsert несёт полный скалярный snapshot — включая Status, диапазон цен и счётчики
(simplicity-first, ADR-0056).
Назначение
Фиксирует создание ProjectEntity (синк Talent, Developers API) и каждое значимое обновление его
публичного скалярного состояния. Событие публичное: даёт downstream-контекстам snapshot проекта без
синхронного запроса в chessboard — карточка и лента витрины (catalog), измерение Project статистики
застройщика (dev-stats, ADR-0057), scope-объект «группа проектов / проект» для прав (identity),
привязка новостей к проекту (news).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.chessboard.project-upserted |
| C# type | ProjectUpsertedKafkaEvent |
| Topic | pin.chessboard.project-upserted.v1 |
| Partitions / key | 3 партиции (проектов — тысячи, низкочастотный поток) / key = Id — порядок per-project гарантирован |
| ContractVersion | 1 |
| Producer | chessboard: Developers API (create / update / publish / hide), синк Talent (talent-chessboard-sync), job project-aggregates-recalc (пересчёт цен/счётчиков), синк справочников handbook-sync (refresh display-имён региона/города, HR-6) |
| AggregateType | Project (агрегат ProjectEntity) |
| Статус | draft |
Событие несёт метаданные: идентификатор сообщения (для дедупликации), время, идентификатор и тип
агрегата-источника (AggregateId = Id, AggregateType = "Project"); сквозной trace восстанавливается из
метаданных. Публикуется атомарно со сменой состояния агрегата (транзакционный outbox).
Payload ProjectUpsertedKafkaEvent
Скалярный snapshot + вложенные snapshot-модели справочника. Дом полей — entity: ProjectEntity;
здесь перечислено только то, что реально в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | ProjectEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
TenantId | Guid | Да | ProjectEntity.TenantId | Immutable | Tenant boundary = DeveloperId (ADR-0052). |
DeveloperId | Guid | Да | ProjectEntity.DeveloperId | Immutable | Застройщик-владелец (родитель иерархии измерений dev-stats). |
Status | string (enum ProjectStatus) | Да | ProjectEntity.Status | Additive only (новые значения) | Draft / Published / Hidden / Archived (reference-data.md); публично видим только Published. |
Name | string | Да | ProjectEntity.Name | Additive only | Название ЖК/отеля (≤ 256, min 1). |
Type | string (enum ProjectType) | Да | ProjectEntity.Type | Additive only | ResidentialComplex / ApartHotel / Hotel / Villa. Фильтр ленты. |
Class | string (enum ProjectClass) | Да | ProjectEntity.Class | Additive only | Standard / Comfort / Business / Premium / Elite. Фильтр ленты. |
ConstructionStatus | string (enum ConstructionStatus) | Да | ProjectEntity.ConstructionStatus | Additive only | Planned / UnderConstruction / Commissioned / Delivered. Фильтр ленты. |
CountryId | short | Да | ProjectEntity.CountryId | Additive only | Страна проекта — ссылка на справочник handbook. |
CountrySnapshot | CountrySnapshot | Да | реплика справочника на момент публикации | Additive only | Display-снапшот страны (snapshots.md). |
RegionId | long? | Нет | ProjectEntity.RegionId | Additive only | Регион — ключ фильтра «регион» ленты; null, пока ссылка не резолвлена (публикация проекта требует резолва, INV-PRJ-3). |
RegionSnapshot | RegionSnapshot? | Нет (заполнен ⇔ RegionId != null) | реплика справочника на момент публикации | Additive only | Display-снапшот региона (snapshots.md). |
CityId | long? | Нет | ProjectEntity.CityId | Additive only | Город проекта; null, пока ссылка не резолвлена (публикация требует резолва, INV-PRJ-3). |
CitySnapshot | CitySnapshot? | Нет (заполнен ⇔ CityId != null) | реплика справочника на момент публикации | Additive only | Display-снапшот города (snapshots.md). |
Stars | short? | Нет | ProjectEntity.Stars | Additive only | Звёздность (1..5); заполняется при Type in (ApartHotel, Hotel). |
HotelierId | Guid? | Нет | ProjectEntity.HotelierId | Additive only | Оператор апарт-отеля (hotelier.md); обязателен для Type in (ApartHotel, Hotel) при публикации (INV-PRJ-4). |
Price | NullableRange<decimal> | Нет | ProjectEntity.Price (price_min/price_max) | Additive only | Диапазон цен свободных объектов недвижимости (Price.Min … Price.Max, price_max >= price_min); денормализация, пересчёт — job project-aggregates-recalc. |
CurrencyId | short | Да | ProjectEntity.CurrencyId | Additive only | Валюта цен проекта — ссылка на справочник валют (одна валюта на проект, INV-PRJ-6; RUB-only, ADR-0048). |
CurrencySnapshot | CurrencySnapshot | Да | реплика справочника на момент публикации | Additive only | Display-снапшот валюты (IsoCode/Name/Sign — snapshots.md); отдельного строкового кода валюты payload не несёт. |
PropertyObjectsTotalCount | int | Да | ProjectEntity.PropertyObjectsTotalCount | Additive only | Всего объектов недвижимости (карточка «кол-во объектов»). |
PropertyObjectsFreeCount | int | Да | ProjectEntity.PropertyObjectsFreeCount | Additive only | Свободных объектов недвижимости (>= 0, <= PropertyObjectsTotalCount). |
PaybackYears | decimal? | Нет | ProjectEntity.PaybackYears | Additive only | Окупаемость в годах (> 0); детальный расчёт — invest-analytics. |
CoverImageFileId | Guid? | Нет | ProjectEntity.CoverImageFileId | Additive only | Обложка карточки (storage/CDN, только FileId). |
Version | long | Да | ProjectEntity.Version | Immutable semantics | Доменная версия; монотонный guard у потребителей (событие с меньшей/равной версией — skip). Version = 1 ⇔ создание проекта. |
Вложенные модели — только snapshot-модели справочника (CountrySnapshot / RegionSnapshot /
CitySnapshot / CurrencySnapshot): дом полей — snapshots.md, здесь
они не переописываются.
Снапшоты фиксируются на момент публикации события и не «догоняются» — переименование записи справочника
уже опубликованные payload-ы не меняет (актуальные имена — refresh display-имён проекта с переопубликацией
события, см. «Когда публикуется»). Пять typed-jsonb вкладок (Location / Infrastructure / Financing /
LegalInfo / ConstructionProgress) в payload не входят — событие несёт скалярный snapshot и служит
сигналом «проект изменён»; полное содержимое вкладок потребители читают через Public API карточки (дом
моделей — project.md). Их изменение тем не менее бампает Version и
публикует событие (см. ниже).
Когда публикуется
| Условие | Переход состояния | Согласованность |
|---|---|---|
| Создание проекта: синк Talent или Developers API создания проекта | [*] → Draft | публикация атомарна со сменой состояния агрегата |
Смена статуса: Publish / Hide / Unhide / Unpublish / Archive (поднимает ProjectStatusChangedEvent доменно) | Draft → Published / Published → Hidden / Hidden → Published / Published → Draft / * → Archived (stateDiagram project.md) | та же |
Обновление значимых полей: Name / Type / Class / ConstructionStatus / гео-ссылок CountryId/RegionId/CityId (с display-именами) / Stars / HotelierId / CoverImageFileId / любой из пяти jsonb-вкладок (Developers API или синк Talent) | без перехода | та же |
Пересчёт агрегатов: job project-aggregates-recalc обновил Price (Min/Max) / PropertyObjectsTotalCount / PropertyObjectsFreeCount по событиям объектов недвижимости | без перехода | та же |
Refresh display-имён: переименование региона/города в справочнике обновило RegionName/CityName проекта — consumer handbook-sync (HR-6, handbook-replica.md); downstream-проекции получают свежие имена в новых снапшотах | без перехода | та же |
Не публикуется при изменении только незначимых полей (audit-поля, LastSyncedAt) | — | — |
Потребители (consumers)
| Consumer-контекст | Эффект (событие → что меняется) | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
catalog | Проекция карточки/ленты витрины: скалярные поля, гео-ссылки + display-имена страны/региона/города из снапшотов (собственной реплики справочника у catalog нет), диапазон цен, счётчики, обложка; overrides отображения — локальные | MessageId + guard по Version | < 1 мин |
dev-stats | Реплика измерения Project (иерархия Developer→Project→…→PropertyObject для обогащения facts, ADR-0057) | то же | < 5 мин |
identity | Реестр scope-объектов «группа проектов / проект» для record-level прав chessboard.projects.manage | то же | < 5 мин |
news | Справочник проектов для привязки новостей (Name/Status для витрины ленты новостей) | то же | < 5 мин |
Доставка и обработка отказов
- Доставка — at-least-once. Транзакционный outbox гарантирует, что событие не теряется при коммите
агрегата, но допускает повторную доставку; порядок сообщений по ключу (
Id) сохраняется в пределах партиции. - Идемпотентность потребителя. Дубликат отсеивается по идентификатору сообщения; переупорядоченное или
устаревшее событие отбрасывается монотонным
Version-guard-ом (событие с версией ≤ применённой — skip), поэтому повторная доставка не меняет реплику. - Ретраи. Временная ошибка потребителя → повтор с backoff до успеха; событие не теряется, порядок per-project не нарушается.
- DLQ. Стабильно необрабатываемое сообщение после исчерпания ретраев уходит в DLQ потребителя с алертом —
чтобы не блокировать партицию; расхождение реплики закрывается повторной обработкой из DLQ либо ресинком
измерения из источника (
chessboard). - Окно согласованности. Пока потребитель не догнал (SLA из таблицы «Потребители»), лента/карточка
витрины, измерение
dev-stats, scope-реестрidentityи справочникnewsвидят прежний snapshot проекта; публикация/скрытие проекта отражается публично с задержкой в пределах SLA (< 1 мин дляcatalog).
Версионирование и совместимость
- Версия — один
ContractVersion = 1; при несовпадении версии события — warn-and-proceed. Топик кодирует версию конвенцией суффикса.v1. - Additive-only для minor: новые поля — optional с безопасным default, без bump.
- Breaking (удаление/переименование/смена типа/смысла поля) — новый
ContractVersion = 2+ новый топикpin.chessboard.project-upserted.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Snapshot-модели справочника (
CountrySnapshot/RegionSnapshot/CitySnapshot/CurrencySnapshot) версионируются вместе с несущим контрактом (отдельногоContractVersionу них нет); их изменения — только additive, неизвестные поля снапшота потребитель игнорирует (snapshots.md). - Unknown enum у потребителя — fallback: warning + сохранение строки как есть;
Type → ResidentialComplex,Class → Comfort,ConstructionStatus → UnderConstruction(fallbacks reference-data.md); дляStatus— fail closed (вести себя какHidden, не показывать публично). - Порядок per-project гарантирован key-ом =
Id; потребители обязаны держатьVersion-guard. - Deprecated-версия — sunset через 2 релиза после миграции последнего потребителя.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
Все идентификаторы (Id, TenantId, DeveloperId, HotelierId, CoverImageFileId), Version | Internal | include |
Status, Name, Type, Class, ConstructionStatus, гео-блок (CountryId/RegionId/CityId + CountrySnapshot/RegionSnapshot/CitySnapshot), Stars | Public | include |
Price, CurrencyId + CurrencySnapshot, PropertyObjectsFreeCount, PaybackYears | Public (в публичных проекциях скрывается от неавторизованных потребителем — правило query-проекций project.md) | include |
Проект PII не содержит (юрлицо-застройщик и декларации LegalInfo — публичны по 214-ФЗ и в payload
не входят), поэтому поле-уровневое маскирование не требуется; в логах идентификаторы не маскируются.
Событие НЕ содержит secrets, tokens, credentials, платёжных реквизитов, unbounded вложений (медиа — только
FileId; содержимое jsonb-вкладок — через API карточки).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Реплика справочников handbook в chessboard (chessboard.handbook_*)
- API (2): POST /api/v1/developers/projects/{projectId}/unpublish — Снятие проекта с публикации, PUT /api/v1/developers/projects/{projectId} — Обновление проекта
- События (2): Event Contract: pin.chessboard.building-upserted (BuildingUpsertedKafkaEvent), Event Contract: pin.chessboard.developer-upserted (DeveloperUpsertedKafkaEvent)
- Консюмеры (4): Consumer: Chessboard Project Deleted (pin-identity-chessboard-projects), Consumer: DevStatsProjectUpsertedConsumer, Consumer: Handbook Sync (pin-chessboard-handbook-sync), Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync)
- Фоновые задачи (2): Background Job: project-aggregates-recalc (пересчёт денормализованных агрегатов), Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Chessboard)
- Поиск (1): Search Document: ProjectSearchDocument
- dimensions (1): dim_project (ClickHouse dimension)
- registries (1): Реестр событий PIN
Связанные документы
- Источник: talent-chessboard-sync (синк) и Developers API
проектов; job
project-aggregates-recalc; refresh display-имён — handbook-sync. - Сущность: project.md; enum-справочники: reference-data.md; соседние события контекста: developer-upserted.md, property-object-upserted.md.
- Snapshot-модели справочника: snapshots.md; реплика справочников — handbook-replica.md.
- ADR-0052 (tenancy), ADR-0056 (simplicity-first), ADR-0057 (dev-stats измерения), ADR-0063 (справочники), ADR-0048 (RUB-only).