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

Event Contract: pin.chessboard.property-object-price-changed (PropertyObjectPriceChangedKafkaEvent)

Версионируемый контракт интеграционного события «цена объекта недвижимости изменилась». Consumer-документы ссылаются сюда, не переописывая payload. Это специализированное append-only ценовое событие того же агрегата, что и property-object-upserted: каждое изменение Price — отдельная иммутабельная зафиксированная точка истории, чтобы высокочастотный потребитель истории цен (invest-analytics) не разбирал полный паспортный snapshot объекта недвижимости ради одного числа. Смена статуса — тоже отдельное событие (property-object-status-changed).

Назначение

Фиксирует каждое изменение PropertyObjectEntity.Price (в т.ч. синком из Talent). Событие публичное: даёт downstream-контекстам ценовую дельту без синхронного запроса в chessboard — append-only история цен и график роста доходности (invest-analytics: цены приходят при обновлении объекта, не удаляются и не редактируются) и обновление цены в проекции витрины (catalog).

Metadata

ПолеЗначение
Event typepin.chessboard.property-object-price-changed
C# typePropertyObjectPriceChangedKafkaEvent
Topicpin.chessboard.property-object-price-changed.v1
Partitions / key6 партиций (высокочастотный ценовой поток по массовому агрегату) / key = PropertyObjectId — порядок ценовых точек по объекту недвижимости гарантирован
ContractVersion1
Producerchessboard: Developers API change-property-object-price (Source = DeveloperApi), синк Talent (talent-chessboard-sync, Source = TalentSync)
Статусdraft

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

Payload PropertyObjectPriceChangedKafkaEvent

Узкий ценовой срез — не паспортный snapshot. Дом полей — entity: PropertyObjectEntity; здесь перечислено только то, что реально в событии. Каждое событие иммутабельно (append-only точка истории), «обязательность» относится к одному сообщению.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
PropertyObjectIdGuidДаPropertyObjectEntity.IdImmutableКлюч агрегата (= Kafka key).
TenantIdGuidДаPropertyObjectEntity.TenantIdImmutableTenant boundary = DeveloperId (ADR-0052).
ProjectIdGuidДаPropertyObjectEntity.ProjectIdImmutableПроект (ключ группировки истории и диапазона цен).
OldPricedecimal?НетPrice до мутацииAdditive onlyПрежняя цена; null только для первой зафиксированной цены объекта недвижимости (создание / первое наблюдение).
NewPricedecimalДаPropertyObjectEntity.Price (после мутации)Additive onlyНовая цена (numeric(14,2), >= 0).
CurrencyIdshortДаPropertyObjectEntity.CurrencyIdAdditive onlyВалюта цены — ссылка на справочник валют (= Project.CurrencyId, INV-PRJ-6; RUB-only, ADR-0048).
CurrencySnapshotCurrencySnapshotДареплика справочника на момент публикацииAdditive onlyDisplay-снапшот валюты (IsoCode/Name/Signsnapshots.md); отдельного строкового кода валюты payload не несёт.
PricePerMeterdecimalДаPropertyObjectEntity.PricePerMeter (после мутации)Additive onlyНовая цена за м² = round(NewPrice / TotalArea, 2) (денормализация агрегата).
ChangedAtDateTimeOffsetДамомент мутации PriceImmutableБизнес-время изменения цены — ось X графика роста.
Sourcestring (enum PropertyObjectStatusSource)ДаPropertyObjectStatusSource инициатораAdditive only (новые значения)Кто изменил цену: DeveloperApi (правка застройщика) или TalentSync (синк Talent). Crm/ExpirationJob цену не меняют. reference-data.md.
VersionlongДаPropertyObjectEntity.VersionImmutable semanticsДоменная версия агрегата после мутации; монотонный guard у потребителей (событие с меньшей/равной версией — skip).
CurrencySnapshot

Единственная вложенная модель — CurrencySnapshot: дом полей — snapshots.md, здесь не переописывается; снапшот фиксируется на момент публикации ценовой точки и не «догоняется» (исторические точки хранят имя валюты на момент факта). Price и CurrencyId в агрегате — плоские колонки (валюта фиксирована на уровне проекта, INV-PRJ-6), поэтому и в событии — плоская ссылка + снапшот.

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

Единственный триггер — фактическая смена PropertyObjectEntity.Price (INV-CHS-008: мутация цены запрещена при Status = Sold; при Reserved/Booked — допускается только Developers API с предупреждающим доменным событием). Статус объекта недвижимости этим событием не меняется (переходы статуса — только события CRM / процесс истечения, INV-CHS-001).

УсловиеЗначение SourceСогласованность
Правка цены менеджером застройщика (Developers API change-property-object-price); NewPrice != PriceDeveloperApiпубликация атомарна со сменой состояния агрегата
Синк Talent: цена из snapshot изменила Price (talent-chessboard-sync)TalentSyncта же
Первичная фиксация цены объекта недвижимости (seed истории при создании / первом наблюдении; OldPrice = null)TalentSync / DeveloperApi по создателюта же
Не публикуется: NewPrice == Price (no-op, естественная идемпотентность) или запрет мутации при Status = Sold (INV-CHS-008)

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

Consumer-контекстЭффект (событие → что меняется)ИдемпотентностьЗадержка (SLA)
invest-analyticsAppend-only факт PropertyObjectPriceHistoryFact в ClickHouse (batch insert, Update/Delete запрещены); из фактов — график роста цены/доходности (chessboard-price-changed)FactId = SourceEventId (идентификатор сообщения) — dedup ReplacingMergeTree< 5 мин
catalogОбновление Price/PricePerMeter проекции объекта недвижимости витрины (ApartmentListItem/ApartmentDetail, шахматка) (chessboard-projection-sync)MessageId + guard по Version< 1 мин

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

  • Доставка — at-least-once. Транзакционный outbox гарантирует, что ценовая точка не теряется при коммите изменения Price, но допускает повторную доставку; порядок точек по ключу (PropertyObjectId) сохраняется в пределах партиции — история цен по объекту недвижимости строго упорядочена по ChangedAt/Version.
  • Идемпотентность append-only истории (invest-analytics). Дубликат отсекается ключом FactId = SourceEventId через ReplacingMergeTree — повторная доставка не создаёт вторую точку в графике роста; Update/Delete фактов запрещены (append-only).
  • Идемпотентность проекции витрины (catalog). Дубликат отсеивается по идентификатору сообщения; устаревшая/переупорядоченная точка отбрасывается монотонным Version-guard-ом (событие с версией ≤ применённой — skip).
  • Порядок между топиками. Ценовое событие живёт в отдельном топике от -upserted/-status-changed; относительный порядок между ними не гарантирован — потребитель держит общий Version-guard по PropertyObjectId.
  • Ретраи и DLQ. Временная ошибка потребителя → повтор с backoff; стабильно необрабатываемая точка после исчерпания ретраев → DLQ потребителя с алертом (партиция не блокируется). Для истории цен пропуск точки устраняется повторной обработкой из DLQ; график роста устойчив к дублям (dedup по FactId).
  • Окно согласованности. Новая цена в проекции витрины видна < 1 мин, точка истории в аналитике — < 5 мин; до этого показывается прежняя цена/прежний график.

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

  • Версия — один ContractVersion = 1; при несовпадении версии события — warn-and-proceed. Топик кодирует версию конвенцией суффикса .v1.
  • Additive-only для minor: новые поля — optional с безопасным default, без bump ContractVersion.
  • Breaking (удаление/переименование/смена типа/смысла поля) — новый ContractVersion = 2 + новый топик pin.chessboard.property-object-price-changed.v2 + dual-publish на период миграции + deadline миграции consumers + sunset .v1.
  • Unknown enum (Source) у потребителя — fallback: warning + сохранение строки как есть (значение используется диагностически, поведение consumer-а от него не зависит).
  • CurrencySnapshot версионируется вместе с несущим контрактом (отдельного ContractVersion нет); изменения snapshot-модели — только additive, неизвестные поля снапшота потребитель игнорирует (snapshots.md).
Планируемое additive-расширение (без bump)

Снапшот измерений (DeveloperId, BuildingId, LayoutGroupId, LayoutId, AreaTotal, OldPricePerMeter) — требование consumer-а invest-analytics (chessboard-price-changed), чтобы аналитика не держала реплику измерений и не ходила в chessboard синхронно. Поля optional → без нарушения совместимости; в v1 payload по дому property-object.md их нет.

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

ПолеКласс данныхПравило
PropertyObjectId, TenantId, ProjectId, Version, ChangedAt, SourceInternalinclude
OldPrice, NewPrice, CurrencyId + CurrencySnapshot, PricePerMeterPublic (в публичных проекциях цена скрывается от неавторизованных потребителем — правило query-проекций property-object.md)include
PII

PII в событии отсутствует: событие описывает объект недвижимости, а не пользователя (участники сделок — контекст crm; actor правки — только в trace, не в payload). Маскирование не требуется — цена не PII; логи могут содержать OldPrice → NewPrice, Source, PropertyObjectId открыто. Событие НЕ содержит secrets, tokens, credentials, платёжных реквизитов, unbounded вложений.

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

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

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