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

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 typepin.chessboard.project-upserted
C# typeProjectUpsertedKafkaEvent
Topicpin.chessboard.project-upserted.v1
Partitions / key3 партиции (проектов — тысячи, низкочастотный поток) / key = Id — порядок per-project гарантирован
ContractVersion1
Producerchessboard: Developers API (create / update / publish / hide), синк Talent (talent-chessboard-sync), job project-aggregates-recalc (пересчёт цен/счётчиков), синк справочников handbook-sync (refresh display-имён региона/города, HR-6)
AggregateTypeProject (агрегат ProjectEntity)
Статусdraft

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

Payload ProjectUpsertedKafkaEvent

Скалярный snapshot + вложенные snapshot-модели справочника. Дом полей — entity: ProjectEntity; здесь перечислено только то, что реально в событии.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
IdGuidДаProjectEntity.IdImmutableКлюч агрегата (= Kafka key).
TenantIdGuidДаProjectEntity.TenantIdImmutableTenant boundary = DeveloperId (ADR-0052).
DeveloperIdGuidДаProjectEntity.DeveloperIdImmutableЗастройщик-владелец (родитель иерархии измерений dev-stats).
Statusstring (enum ProjectStatus)ДаProjectEntity.StatusAdditive only (новые значения)Draft / Published / Hidden / Archived (reference-data.md); публично видим только Published.
NamestringДаProjectEntity.NameAdditive onlyНазвание ЖК/отеля (≤ 256, min 1).
Typestring (enum ProjectType)ДаProjectEntity.TypeAdditive onlyResidentialComplex / ApartHotel / Hotel / Villa. Фильтр ленты.
Classstring (enum ProjectClass)ДаProjectEntity.ClassAdditive onlyStandard / Comfort / Business / Premium / Elite. Фильтр ленты.
ConstructionStatusstring (enum ConstructionStatus)ДаProjectEntity.ConstructionStatusAdditive onlyPlanned / UnderConstruction / Commissioned / Delivered. Фильтр ленты.
CountryIdshortДаProjectEntity.CountryIdAdditive onlyСтрана проекта — ссылка на справочник handbook.
CountrySnapshotCountrySnapshotДареплика справочника на момент публикацииAdditive onlyDisplay-снапшот страны (snapshots.md).
RegionIdlong?НетProjectEntity.RegionIdAdditive onlyРегион — ключ фильтра «регион» ленты; null, пока ссылка не резолвлена (публикация проекта требует резолва, INV-PRJ-3).
RegionSnapshotRegionSnapshot?Нет (заполнен ⇔ RegionId != null)реплика справочника на момент публикацииAdditive onlyDisplay-снапшот региона (snapshots.md).
CityIdlong?НетProjectEntity.CityIdAdditive onlyГород проекта; null, пока ссылка не резолвлена (публикация требует резолва, INV-PRJ-3).
CitySnapshotCitySnapshot?Нет (заполнен ⇔ CityId != null)реплика справочника на момент публикацииAdditive onlyDisplay-снапшот города (snapshots.md).
Starsshort?НетProjectEntity.StarsAdditive onlyЗвёздность (1..5); заполняется при Type in (ApartHotel, Hotel).
HotelierIdGuid?НетProjectEntity.HotelierIdAdditive onlyОператор апарт-отеля (hotelier.md); обязателен для Type in (ApartHotel, Hotel) при публикации (INV-PRJ-4).
PriceNullableRange<decimal>НетProjectEntity.Price (price_min/price_max)Additive onlyДиапазон цен свободных объектов недвижимости (Price.MinPrice.Max, price_max >= price_min); денормализация, пересчёт — job project-aggregates-recalc.
CurrencyIdshortДаProjectEntity.CurrencyIdAdditive onlyВалюта цен проекта — ссылка на справочник валют (одна валюта на проект, INV-PRJ-6; RUB-only, ADR-0048).
CurrencySnapshotCurrencySnapshotДареплика справочника на момент публикацииAdditive onlyDisplay-снапшот валюты (IsoCode/Name/Signsnapshots.md); отдельного строкового кода валюты payload не несёт.
PropertyObjectsTotalCountintДаProjectEntity.PropertyObjectsTotalCountAdditive onlyВсего объектов недвижимости (карточка «кол-во объектов»).
PropertyObjectsFreeCountintДаProjectEntity.PropertyObjectsFreeCountAdditive onlyСвободных объектов недвижимости (>= 0, <= PropertyObjectsTotalCount).
PaybackYearsdecimal?НетProjectEntity.PaybackYearsAdditive onlyОкупаемость в годах (> 0); детальный расчёт — invest-analytics.
CoverImageFileIdGuid?НетProjectEntity.CoverImageFileIdAdditive onlyОбложка карточки (storage/CDN, только FileId).
VersionlongДаProjectEntity.VersionImmutable semanticsДоменная версия; монотонный guard у потребителей (событие с меньшей/равной версией — skip). Version = 1 ⇔ создание проекта.

Вложенные модели — только snapshot-модели справочника (CountrySnapshot / RegionSnapshot / CitySnapshot / CurrencySnapshot): дом полей — snapshots.md, здесь они не переописываются.

Снапшоты не «догоняются»; jsonb-вкладки в payload не входят

Снапшоты фиксируются на момент публикации события и не «догоняются» — переименование записи справочника уже опубликованные 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), VersionInternalinclude
Status, Name, Type, Class, ConstructionStatus, гео-блок (CountryId/RegionId/CityId + CountrySnapshot/RegionSnapshot/CitySnapshot), StarsPublicinclude
Price, CurrencyId + CurrencySnapshot, PropertyObjectsFreeCount, PaybackYearsPublic (в публичных проекциях скрывается от неавторизованных потребителем — правило query-проекций project.md)include
PII отсутствует

Проект PII не содержит (юрлицо-застройщик и декларации LegalInfo — публичны по 214-ФЗ и в payload не входят), поэтому поле-уровневое маскирование не требуется; в логах идентификаторы не маскируются. Событие НЕ содержит secrets, tokens, credentials, платёжных реквизитов, unbounded вложений (медиа — только FileId; содержимое jsonb-вкладок — через API карточки).

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

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

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