Event Contract: pin.chessboard.property-object-status-changed (PropertyObjectStatusChangedKafkaEvent)
Версионируемый контракт интеграционного события «статус продажи объекта недвижимости изменился».
Consumer-документы ссылаются сюда, не переописывая payload. Специализированное событие того же агрегата
PropertyObjectEntity: несёт только переход статуса + таймеры, а не полный паспортный snapshot (тот —
property-object-upserted) и не изменение цены
(property-object-price-changed). Разделение позволяет высокочастотным
потребителям (индикация шахматки/витрины, пометка «продан») реагировать на статус, не разбирая весь
snapshot объекта недвижимости.
Назначение
Фиксирует каждый переход PropertyObjectEntity.Status (INV-CHS-001) — Свободен → Резерв → Бронь → Продан, обратные release-ы, а также Свободен ↔ Скрыт и Свободен → Продан (продажа вне PIN). Событие
публичное: даёт downstream-контекстам актуальный статус объекта недвижимости и моменты истечения холдов без
синхронного запроса в chessboard — цветовая индикация ячейки шахматки и доступности объекта недвижимости в
витрине, пометка «продан»/«снят» в избранном и подборках, уведомления инвестора об отслеживаемом объекте
недвижимости. Источник истины текущего статуса — шахматка (CRM лишь инициирует переходы событиями).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.chessboard.property-object-status-changed |
| C# type | PropertyObjectStatusChangedKafkaEvent |
| Topic | pin.chessboard.property-object-status-changed.v1 |
| Partitions / key | 6 партиций (объекты недвижимости — самый массовый агрегат шахматки, сотни тысяч) / key = PropertyObjectId — порядок переходов по объекту недвижимости гарантирован |
| ContractVersion | 1 |
| Producer | chessboard: consumer crm-property-object-status (Crm), Developers API Hide/Unhide (DeveloperApi), процесс property-object-hold-expiration (ExpirationJob), синк talent-chessboard-sync (TalentSync) |
| Статус | draft |
Событие несёт метаданные: идентификатор сообщения (для дедупликации), время, идентификатор и тип
агрегата-источника (AggregateId = PropertyObjectId, AggregateType = "PropertyObject"); сквозной trace
восстанавливается из метаданных. Публикуется атомарно со сменой состояния агрегата, в том же изменении,
что и переход (транзакционный outbox).
Payload PropertyObjectStatusChangedKafkaEvent
Дельта перехода статуса. Дом полей — entity: PropertyObjectEntity; здесь
перечислено только то, что реально в событии. FromStatus/ToStatus берутся из доменного события перехода
(From, To, StatusSource).
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
PropertyObjectId | Guid | Да | PropertyObjectEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
TenantId | Guid | Да | PropertyObjectEntity.TenantId | Immutable | Tenant boundary = DeveloperId (ADR-0052). |
ProjectId | Guid | Да | PropertyObjectEntity.ProjectId | Immutable | Проект — адресация проекции витрины и пересчёта агрегатов. |
BuildingId | Guid | Да | PropertyObjectEntity.BuildingId | Immutable | Здание — адресация ячейки шахматки (building_id, floor, section). |
FromStatus | string (enum PropertyObjectStatus) | Да | статус до перехода | Additive only (новые значения enum) | Статус до перехода: Free/Reserved/Booked/Sold/Hidden. |
ToStatus | string (enum PropertyObjectStatus) | Да | PropertyObjectEntity.Status (новый) | Additive only | Статус после перехода; терминальный — Sold. |
StatusSource | string (enum PropertyObjectStatusSource) | Да | PropertyObjectEntity.StatusSource | Additive only | Кто выполнил переход: Crm/TalentSync/DeveloperApi/ExpirationJob (reference-data.md). |
ReservedUntil | DateTimeOffset? | Нет | PropertyObjectEntity.ReservedUntil | Additive only | Момент истечения резерва; заполнен ⇔ ToStatus = Reserved (INV-CHS-002), иначе null. |
BookedUntil | DateTimeOffset? | Нет | PropertyObjectEntity.BookedUntil | Additive only | Момент истечения брони; заполнен ⇔ ToStatus = Booked, иначе null. |
HoldDealId | Guid? | Нет | PropertyObjectEntity.HoldDealId | Additive only | Id сделки CRM, удерживающей объект недвижимости; заполнен ⇔ ToStatus in (Reserved, Booked), иначе null. |
SoldAt | DateTimeOffset? | Нет | PropertyObjectEntity.SoldAt | Additive only | Момент продажи; заполнен ⇔ ToStatus = Sold, иначе null. |
StatusChangedAt | DateTimeOffset | Да | PropertyObjectEntity.StatusChangedAt | Immutable semantics | Момент перехода (для таймлайнов и диагностики залипших таймеров). |
Version | long | Да | PropertyObjectEntity.Version | Immutable semantics | Доменная версия агрегата; монотонный guard у потребителей (событие с меньшей/равной версией — skip). |
Вложенных моделей нет. Паспортные поля (Number/Floor/площади/Price) в событие не входят (дом —
property-object-upserted); потребитель, которому нужен
snapshot, ведёт локальную реплику по PropertyObjectId из upserted-события.
Когда публикуется
Событие публикуется на каждый переход Status (INV-CHS-001), атомарно со сменой состояния объекта
недвижимости.
При обновлении только паспортных полей публикуется property-object-upserted;
при изменении только цены — property-object-price-changed; если Status
не изменился — событие не публикуется.
| Условие (источник перехода) | Переход состояния | Согласованность |
|---|---|---|
События CRM через consumer crm-property-object-status (StatusSource = Crm) | Free → Reserved (Reserve), Reserved → Booked (Book), Reserved|Booked → Free (Release), Booked → Sold (MarkSold) | публикация атомарна со сменой состояния агрегата |
Developers API управления объектом недвижимости (StatusSource = DeveloperApi) | Free → Hidden (Hide, INV-CHS-009), Hidden → Free (Unhide) | та же |
Страховочный процесс property-object-hold-expiration (StatusSource = ExpirationJob) | Reserved|Booked → Free (Release по истечении таймера + grace 10 мин — потерян release CRM) | та же |
Синк Talent Chessboard (StatusSource = TalentSync) | Free → Sold (MarkSold — продажа вне PIN) | та же |
Потребители (consumers)
| Consumer-контекст | Эффект (событие → что меняется) | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
catalog | Индикация статуса в шахматке (цвет ячейки) и доступности объекта недвижимости в витрине; Hidden/Sold убираются из публичных срезов | MessageId + guard по Version | < 1 мин |
favorites | Пометка «продан»/«снят» у объекта недвижимости в избранном инвестора | то же | < 5 мин |
selections | Пометка «продан» у объекта недвижимости в подборках агента/инвестора | то же | < 5 мин |
notifications | Уведомление инвестора об изменении статуса отслеживаемого объекта недвижимости (продан / снова свободен) | то же | < 5 мин |
Доставка и обработка отказов
- Доставка — at-least-once. Транзакционный outbox гарантирует, что переход не теряется при коммите смены
состояния, но допускает повторную доставку; порядок переходов по ключу (
PropertyObjectId) сохраняется в пределах партиции. - Идемпотентность потребителя. Дубликат отсеивается по идентификатору сообщения; переупорядоченный или
устаревший переход отбрасывается монотонным
Version-guard-ом (событие с версией ≤ применённой — skip). Итог: повторная доставкаFree → Soldили release не «мигает» статусом у потребителя. - Ретраи. Временная ошибка потребителя → повтор с backoff до успеха; событие не теряется, порядок переходов per-object не нарушается.
- DLQ. Стабильно необрабатываемый переход после исчерпания ретраев уходит в DLQ потребителя с алертом —
чтобы не блокировать партицию; расхождение закрывается повторной обработкой из DLQ либо ресинком из
источника (
chessboard— source of truth статуса). - Окно согласованности. Между переходом и обновлением проекции ячейка шахматки/доступность объекта
недвижимости у потребителя кратковременно показывают прежний статус (SLA
catalog< 1 мин, пометки «продан» в избранном/подборках и уведомления — < 5 мин); source of truth текущего статуса — всегдаchessboard.
Версионирование и совместимость
- Версия — один
ContractVersion = 1; при несовпадении версии события — warn-and-proceed. Топик кодирует версию конвенцией суффикса.v1. - Additive-only для minor: новые поля — optional с безопасным default, без bump.
- Breaking (удаление/переименование/смена типа/смысла поля) — новый
ContractVersion = 2+ новый топикpin.chessboard.property-object-status-changed.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Unknown enum (
FromStatus/ToStatus—PropertyObjectStatus;StatusSource) у потребителя — fallback: warning + сохранение строки как есть; для неизвестногоToStatusвести себя как для наиболее ограничивающего значенияHidden(fail closed — убрать объект недвижимости из публичных срезов). - Порядок переходов по объекту недвижимости гарантирован key-ом
PropertyObjectIdвнутри этого топика; относительно property-object-upserted/property-object-price-changed (другие топики) порядок не гарантирован — потребитель обязан держать монотонныйVersion-guard поPropertyObjectId. - Deprecated-версия — sunset через 2 релиза после миграции последнего потребителя.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
PropertyObjectId, TenantId, ProjectId, BuildingId, Version, StatusChangedAt | Internal | include |
FromStatus, ToStatus, StatusSource | Public/Internal | include (статус публичен в шахматке; фильтрация Hidden/Sold для неавторизованных — на стороне query-проекций потребителя) |
ReservedUntil, BookedUntil, SoldAt | Public | include (обратный отсчёт/дата продажи в UI витрины) |
HoldDealId | Internal | include только для доверенных consumer-ов (ACL топика) — id сделки чужого контекста (crm), внутренний ключ идемпотентности; в публичные проекции витрины не выводится |
Событие НЕ содержит secrets, tokens, PII (участники сделки — контекст crm), платёжных данных,
unbounded вложений. PII у объекта недвижимости отсутствует.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): POST /api/v1/developers/property-objects/{propertyObjectId}/hide — Скрытие объекта недвижимости, POST /api/v1/developers/property-objects/{propertyObjectId}/unhide — Возврат объекта недвижимости в продажу
- События (2): Event Contract: pin.chessboard.property-object-price-changed (PropertyObjectPriceChangedKafkaEvent), Event Contract: pin.chessboard.property-object-upserted (PropertyObjectUpsertedKafkaEvent)
- Консюмеры (2): Consumer: CRM PropertyObject Status (pin-chessboard-crm-property-object-status), Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync)
- Фоновые задачи (2): Background Job: property-object-hold-expiration (страховка от потерянных release-событий CRM), Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Chessboard)
- scenarios (1): UC-OVR-003. Бронирование объекта у агента (+3a)
- registries (1): Реестр событий PIN
Связанные документы
- Producers: consumer crm-property-object-status (переходы
Crm), Developers API управления объектами недвижимости (Hide/Unhide), процесс property-object-hold-expiration (releaseExpirationJob), синк talent-chessboard-sync (Free → Sold). - Сущность и state-машина: property-object.md (INV-CHS-001/002/003,
stateDiagram); enum — reference-data.md (
PropertyObjectStatus,PropertyObjectStatusSource). - Соседние события агрегата: property-object-upserted.md, property-object-price-changed.md.
- ADR-0052, ADR-0056.