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 type | pin.chessboard.property-object-price-changed |
| C# type | PropertyObjectPriceChangedKafkaEvent |
| Topic | pin.chessboard.property-object-price-changed.v1 |
| Partitions / key | 6 партиций (высокочастотный ценовой поток по массовому агрегату) / key = PropertyObjectId — порядок ценовых точек по объекту недвижимости гарантирован |
| ContractVersion | 1 |
| Producer | chessboard: 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 | Описание |
|---|---|---|---|---|---|
PropertyObjectId | Guid | Да | PropertyObjectEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
TenantId | Guid | Да | PropertyObjectEntity.TenantId | Immutable | Tenant boundary = DeveloperId (ADR-0052). |
ProjectId | Guid | Да | PropertyObjectEntity.ProjectId | Immutable | Проект (ключ группировки истории и диапазона цен). |
OldPrice | decimal? | Нет | Price до мутации | Additive only | Прежняя цена; null только для первой зафиксированной цены объекта недвижимости (создание / первое наблюдение). |
NewPrice | decimal | Да | PropertyObjectEntity.Price (после мутации) | Additive only | Новая цена (numeric(14,2), >= 0). |
CurrencyId | short | Да | PropertyObjectEntity.CurrencyId | Additive only | Валюта цены — ссылка на справочник валют (= Project.CurrencyId, INV-PRJ-6; RUB-only, ADR-0048). |
CurrencySnapshot | CurrencySnapshot | Да | реплика справочника на момент публикации | Additive only | Display-снапшот валюты (IsoCode/Name/Sign — snapshots.md); отдельного строкового кода валюты payload не несёт. |
PricePerMeter | decimal | Да | PropertyObjectEntity.PricePerMeter (после мутации) | Additive only | Новая цена за м² = round(NewPrice / TotalArea, 2) (денормализация агрегата). |
ChangedAt | DateTimeOffset | Да | момент мутации Price | Immutable | Бизнес-время изменения цены — ось X графика роста. |
Source | string (enum PropertyObjectStatusSource) | Да | PropertyObjectStatusSource инициатора | Additive only (новые значения) | Кто изменил цену: DeveloperApi (правка застройщика) или TalentSync (синк Talent). Crm/ExpirationJob цену не меняют. reference-data.md. |
Version | long | Да | PropertyObjectEntity.Version | Immutable semantics | Доменная версия агрегата после мутации; монотонный guard у потребителей (событие с меньшей/равной версией — skip). |
Единственная вложенная модель — 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 != Price | DeveloperApi | публикация атомарна со сменой состояния агрегата |
Синк 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-analytics | Append-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).
Снапшот измерений (DeveloperId, BuildingId, LayoutGroupId, LayoutId, AreaTotal,
OldPricePerMeter) — требование consumer-а invest-analytics
(chessboard-price-changed), чтобы аналитика
не держала реплику измерений и не ходила в chessboard синхронно. Поля optional → без нарушения
совместимости; в v1 payload по дому property-object.md их нет.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
PropertyObjectId, TenantId, ProjectId, Version, ChangedAt, Source | Internal | include |
OldPrice, NewPrice, CurrencyId + CurrencySnapshot, PricePerMeter | Public (в публичных проекциях цена скрывается от неавторизованных потребителем — правило query-проекций property-object.md) | include |
PII в событии отсутствует: событие описывает объект недвижимости, а не пользователя (участники сделок —
контекст crm; actor правки — только в trace, не в payload). Маскирование не требуется — цена не PII; логи
могут содержать OldPrice → NewPrice, Source, PropertyObjectId открыто. Событие НЕ содержит secrets,
tokens, credentials, платёжных реквизитов, unbounded вложений.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): POST /api/v1/developers/buildings/{buildingId}/property-objects/bulk — Массовый upsert объектов недвижимости, POST /api/v1/developers/property-objects/{propertyObjectId}/price — Изменение цены объекта недвижимости
- События (2): Event Contract: pin.chessboard.property-object-status-changed (PropertyObjectStatusChangedKafkaEvent), Event Contract: pin.chessboard.property-object-upserted (PropertyObjectUpsertedKafkaEvent)
- Консюмеры (3): Consumer: ChessboardPropertyObjectPriceChangedConsumer, Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync), Talent Price Sync: сведён к consumer-у chessboard-price-changed (решение, не отдельный consumer)
- Фоновые задачи (1): Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Chessboard)
- registries (1): Реестр событий PIN
Связанные документы
- Сущность: property-object.md (INV-CHS-008,
ChangePrice,PricePerMeter);Source— enumPropertyObjectStatusSource(reference-data.md). - Источники: Developers API change-property-object-price.md
(
Source = DeveloperApi, UC-CHS-003), синк talent-chessboard-sync (Source = TalentSync) — история цен едина, различается полемSource. - Соседние события агрегата: property-object-upserted.md (паспортный snapshot, не цена), property-object-status-changed.md (переходы статуса).
- Потребители: chessboard-price-changed (история цен), chessboard-projection-sync (витрина).
- Snapshot-модель валюты: snapshots.md; реплика справочников — handbook-replica.md.
- ADR-0052, ADR-0056, ADR-0063 (справочники), ADR-0048 (RUB-only).