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

Event Contract: pin.chessboard.developer-upserted (DeveloperUpsertedKafkaEvent)

Версионируемый контракт интеграционного события «застройщик создан/обновлён». Consumer-документы ссылаются сюда, не переописывая payload. Это «developer-upserted»-событие шахматки: публичный snapshot профиля девелопера — верх иерархии Developer → Project → Building → PropertyObject. У агрегата нет отдельного status-события: смена публичности (Draft/Published/Hidden) едет тем же upsert-ом (в отличие от объекта недвижимости — property-object-upserted), так как поток изменений застройщиков низкочастотный.

Назначение

Фиксирует создание DeveloperEntity (consumer компаний identity при Type = Developer, синк Talent Chessboard, admin-операция) и каждое значимое обновление публичных полей карточки (Name / Status / LogoFileId / Highlights). Событие публичное: даёт downstream-контекстам snapshot застройщика без синхронного запроса в chessboard — карточка девелопера витрины, верхнее измерение статистики (ADR-0057), отображение застройщика в сделках CRM, контекст правил вознаграждений.

Metadata

ПолеЗначение
Event typepin.chessboard.developer-upserted
C# typeDeveloperUpsertedKafkaEvent
Topicpin.chessboard.developer-upserted.v1
Partitions / key3 партиции (застройщиков сотни — низкая кардинальность) / key = Id — порядок per-developer гарантирован
ContractVersion1
Producerchessboard: Developers API управления профилем, consumer компаний identity-companies (pin.identity.companies.v1, Type = Developer), синк talent-chessboard-sync
AggregateTypeDeveloper (агрегат DeveloperEntity)
Статусdraft

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

Payload DeveloperUpsertedKafkaEvent

Публичный snapshot карточки застройщика. Дом полей — entity: DeveloperEntity; здесь перечислено только то, что реально в событии.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
IdGuidДаDeveloperEntity.IdImmutableКлюч агрегата (= Kafka key). Он же CompanyId в identity и TenantId дочерних сущностей шахматки (ADR-0052).
Statusstring (enum DeveloperStatus)ДаDeveloperEntity.StatusAdditive only (новые значения enum)Draft / Published / Hidden — публичность профиля. Hidden каскадно скрывает карточку и все проекты (INV-DEV-6).
NamestringДаDeveloperEntity.NameAdditive onlyПубличное название застройщика (≤ 256, min 1). Реплика-поле при SyncSource = TalentChessboard.
LegalNamestring?НетDeveloperEntity.LegalNameAdditive onlyЮридическое наименование (карточка/декларации; ≤ 512).
LogoFileIdGuid?НетDeveloperEntity.LogoFileIdAdditive onlyФайл логотипа во внешнем storage; выдача ссылок — граница storage/CDN.
FoundedYearshort?НетDeveloperEntity.FoundedYearAdditive onlyГод основания (1900..2100) — блок «опыт застройщика».
HighlightsDeveloperHighlightsDocumentДаDeveloperEntity.Highlights (typed jsonb)Additive onlyПоказатели публичной карточки (см. вложенную модель ниже).
VersionlongДаDeveloperEntity.VersionImmutable semanticsДоменная версия агрегата; монотонный guard у потребителей (событие с меньшей/равной версией — skip). Version = 1 ⇔ создание профиля.

Вложенная модель DeveloperHighlightsDocument

Typed-jsonb, дом — developer.md, раздел «Модели JSONB» (ContractVersion = 1); здесь — состав, реально едущий в событии.

ПолеТипОбязательностьОграниченияОписание
ContractVersionintДа= 1Версия схемы документа (независима от ContractVersion события).
ObjectsDeliveredCountint?Нет>= 0Сдано объектов.
ObjectsInProgressCountint?Нет>= 0Строится объектов.
YearsOnMarketshort?Нет0..200; null — фронт вычисляет из FoundedYearЛет на рынке.
ReliabilityValuedecimal?Нет0..ReliabilityMax, precision (4,1)Показатель надёжности.
ReliabilityMaxdecimalДаdefault 10, precision (4,1)Верхняя граница шкалы надёжности.
AwardTitlesIReadOnlyList<string>Да (может быть пустым)элемент ≤ 256, ≤ 20 элементовНаграды/титулы.
Поля вне payload

Служебные и локально-денормализованные поля агрегата (SyncSource, ExternalTalentDeveloperId, ReplicaVersion, LastSyncedAt, ProjectsPublishedCount, audit) в payload не входят — они внутренние для owning-контекста. Description, Website, ContactPhone, ContactEmail в minimal-snapshot v1 не включены (организационные, не PII); при потребности витрины добавляются additive без bump.

Когда публикуется

УсловиеПереход состоянияСогласованность
Создание профиля: consumer компаний identity (Type = Developer), синк Talent Chessboard или admin-операция[*] → Draftпубликация атомарна со сменой состояния агрегата
Значимое обновление Status: Publish / Hide / Unhide / Unpublish (Developers API), suspend компании в identity (Published → Hidden)переход по stateDiagram developer.mdта же
Значимое обновление Name / LogoFileId (синк Talent — реплика-поля; Developers API — при SyncSource = Pin) или Highlights (Developers API — PIN-поле)без переходата же
Не публикуется при изменении только LegalName / FoundedYear / Description / Website / контактов — низкоценные/служебные поля; свежее значение LegalName/FoundedYear переносится в snapshot при следующем значимом изменении или создании
Локальное событие статуса

Переход Status дополнительно поднимает доменное DeveloperStatusChangedEvent для локальных обработчиков; наружу идёт только этот upsert (отдельного status-топика нет).

Потребители (consumers)

Consumer-контекстЭффект (событие → что меняется)ИдемпотентностьЗадержка (SLA)
catalogПроекция карточки застройщика витрины (название, лого, Highlights); Hidden убирает карточку и проекты из public-проекцийMessageId + guard по Version< 1 мин
dev-statsРеплика верхнего измерения Developer иерархии Developer→Project→Building→LayoutGroup→Layout→PropertyObject (ADR-0057)то же< 5 мин
crmОтображение застройщика в сделках/карточках объектов (название, лого)то же< 5 мин
rewardsКонтекст застройщика для правил вознаграждений (привязка программ к девелоперу)то же< 5 мин

Доставка и обработка отказов

  • Доставка — at-least-once. Транзакционный outbox гарантирует, что событие не теряется при коммите агрегата, но допускает повторную доставку; порядок сообщений по ключу (Id) сохраняется в пределах партиции.
  • Идемпотентность потребителя. Дубликат отсеивается по идентификатору сообщения; переупорядоченное или устаревшее событие отбрасывается монотонным Version-guard-ом (событие с версией ≤ применённой — skip), поэтому повторная доставка не меняет реплику.
  • Ретраи. Временная ошибка потребителя (недоступность его БД/зависимости) → повтор с backoff до успеха; событие не теряется, порядок per-developer не нарушается.
  • DLQ. Стабильно необрабатываемое сообщение («ядовитое») после исчерпания ретраев уходит в DLQ потребителя с алертом — чтобы не блокировать партицию; расхождение реплики закрывается повторной обработкой из DLQ либо ресинком из источника (chessboard).
  • Окно согласованности. Пока потребитель не догнал (SLA из таблицы «Потребители»), downstream показывает прежний профиль застройщика; каскад Hidden (скрытие карточки и проектов) виден потребителям в пределах их SLA — до этого проекты застройщика ещё отдаются публичными проекциями.

Версионирование и совместимость

  • Версия — один ContractVersion = 1; при несовпадении версии события — warn-and-proceed. Топик кодирует версию конвенцией суффикса .v1.
  • Additive-only для minor: новые поля — optional с безопасным default, без bump (тот же принцип для DeveloperHighlightsDocument — новое поле nullable/default той же ContractVersion).
  • Breaking (удаление/переименование/смена типа/смысла поля) — новый ContractVersion = 2 + новый топик pin.chessboard.developer-upserted.v2 + dual-publish на период миграции + deadline миграции consumers + sunset .v1.
  • Unknown enum Status у потребителя — fallback: warning + метрика chessboard_sync_unknown_enum_total, сохранение строки как есть; поведение — как для наиболее ограничивающего значения Hidden (не показывать, fail closed).
  • Deprecated-версия — sunset через 2 релиза после миграции последнего потребителя.

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

ПолеКласс данныхПравило
Id, VersionInternalinclude
Status, Name, LegalName, FoundedYear, Highlights.*Public (данные о застройщике публичны — карточка каталога, юрданные по 214-ФЗ)include
LogoFileIdInternal (ссылка на файл; сам файл — storage/CDN, не в событии)include
PII отсутствует

PII в событии нет: застройщик — организация, контакты организационные и в payload не включены, поэтому маскирование не требуется. Событие НЕ содержит secrets, tokens, raw credentials, персональных/платёжных реквизитов, unbounded вложений (лого — только FileId). Если организационные контакты когда-либо добавят additive — они остаются классом Public (не PII конкретного лица) и не требуют маскирования.

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

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

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