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

Entity / Aggregate: DealTimelineEventEntity (TPH)

Дом полей сущности «Событие таймлайна сделки» контекста crm — вкладка «Ход сделки» (US-INV-09/10/11, US-AGT-05; глоссарий «Таймлайн сделки»). Все проекции (лента карточки сделки, счётчики «новых событий», карточки чата messenger) ссылаются сюда и описывают только дельту.

Назначение

DealTimelineEventEntity — append-only лента фактов сделки с датами и статусом выполнения (выполнено / в процессе / просрочено — US-INV-09). Записывается в одном согласованном изменении с породившей мутацией агрегата: доменные события сделки/брони/документов материализуются в строки таймлайна. Таблица использует single-table наследование (TPH) по дискриминатору Type; типизированные данные — versioned jsonb Payload (модели ниже).

Отличие от ленты лида

Таймлайн сделки видят все участники сделки (инвестор, агент, застройщик, Менеджер ПИН); лента лида (lead.md LeadEventEntity) видна только агентской стороне.

Классификация

ПолеЗначение
Контекстcrm
KindEntity (append-only, в составе агрегата сделки; отдельная таблица)
Source of truthcrm
Таблицаcrm.deal_timeline_events (TPH)
Tenant isolationTenantId = DeveloperCompanyId (наследуется от сделки)

Поля (базовая часть TPH)

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор события.
TenantIdGuidДаindexиз сделкиЗастройщик сделки.
DealIdGuidДаFK fk_deal_timeline_deal_id → crm.deals(id), index ix_deal_timeline_deal_occurred (DealId,OccurredAt)appСделка (deal.md).
TypeDealTimelineEventTypeДаdiscriminator TPH, enum как text, varchar(64), index (deal_id,type)appТип события (см. enum).
StatusTimelineItemStatusДаenum как text, varchar(32)app / workerИконка статуса US-INV-09: Done / InProgress / Overdue.
TitlestringДаvarchar(256)app (генерация по типу)Отображаемый заголовок («Создана заявка на бронирование»).
OccurredAtDateTimeOffsetДачасть индекса лентыappКогда факт произошёл (сортировка «от старых к новым», US-INV-09).
DueAtDateTimeOffset?Нетpartial index where status = 'InProgress' (worker)appДедлайн для InProgress-шагов; просрочка → Overdue (polling-worker, ADR-0056).
CompletedAtDateTimeOffset?НетappКогда шаг закрыт (→ Done).
ActorUserIdGuid?НетappКто совершил действие; null — система/worker.
ActorRoleParticipantRole?Нетenum как text, varchar(32); enum — deal.mdappРоль актора на момент события.
Payloadtyped jsonb per-typeДаjsonb; модели ниже; ContractVersionappТипизированные данные события.
CreatedAtDateTimeOffsetДаappЗапись строки.
UpdatedAtDateTimeOffsetДаapp / workerДата обновления Status.

Enum TimelineItemStatus (US-INV-09.3)

ЗначениеОписание
DoneВыполнено (факт свершился / целевое действие закрыто).
InProgressВ процессе: событие требует действия (например «загрузите документы») и ещё не закрыто.
OverdueПросрочено: DueAt < now, действие не выполнено (переводит worker timeline-overdue).

Enum DealTimelineEventType и typed payload (jsonb)

Версионирование payload

Все payload-модели — ContractVersion = 1; правило совместимости — additive-only, breaking-изменение требует нового ContractVersion. Поля с ? — nullable, остальные обязательны.

ТипКогда пишется (источник)Payload-модель
DealCreatedсоздание сделкиTimelineDealCreatedPayload { LeadId: Guid, FixationRequestId: Guid?, PropertyObjectId: Guid, PaymentMethod: string }
StageChangedDeal.MoveToStepTimelineStageChangedPayload { FromStepId: Guid, FromStepName: string, ToStepId: Guid, ToStepName: string, ToStepGroup: string }
StatusChangedсмена Deal.StatusTimelineStatusChangedPayload { FromStatus: string, ToStatus: string, Reason: string? }
PropertyObjectChangedDeal.ChangePropertyObject (ТЗ.md:457)TimelinePropertyObjectChangedPayload { OldPropertyObjectId: Guid, NewPropertyObjectId: Guid, OldPriceAmount: decimal, NewPriceAmount: decimal, Currency: string }
MemberAddedDeal.AddMemberTimelineMemberPayload { UserId: Guid, Role: string }
MemberRemovedDeal.RemoveMemberTimelineMemberPayload (та же модель)
BookingRequestedсоздание брони (booking.md); клик раскрывает условия (US-INV-10)TimelineBookingRequestedPayload { BookingId: Guid, PropertyObjectId: Guid, ProgramName: string, IsPaid: bool, PriceAmount: decimal?, Currency: string, ReserveDurationMinutes: int, BookingDurationHours: int, RefundPolicy: string? }
BookingReserveExpiredworker истечения резерваTimelineBookingRefPayload { BookingId: Guid }
BookingPaidConfirmPayment (US-INV-06)TimelineBookingPaidPayload { BookingId: Guid, Amount: decimal, Currency: string, PaidAt: DateTimeOffset }
BookingConfirmed→ BookedTimelineBookingConfirmedPayload { BookingId: Guid, BookingExpiresAt: DateTimeOffset }
BookingExpiredworker истечения брониTimelineBookingRefPayload
BookingExtendedBooking.ExtendTimelineBookingExtendedPayload { BookingId: Guid, NewBookingExpiresAt: DateTimeOffset }
DocumentRequestedзапрос документа (InProgress + DueAt)TimelineDocumentPayload { DocumentId: Guid, DocumentType: string, Title: string, RequestComment: string? }
DocumentUploadedзагрузка файла (закрывает соответствующий DocumentRequestedDone)TimelineDocumentPayload
DocumentAcceptedReview(Accept)TimelineDocumentPayload
DocumentRejectedReview(Reject)TimelineDocumentRejectedPayload { DocumentId: Guid, DocumentType: string, RejectReason: string }
EscrowProvidedинвестор указал эскроу (US-INV-19)TimelineEscrowPayload { BankId: Guid?, AccountNumberMasked: string } — только маска ****NNNN
ContractsPreparedUS-DEV-04 / US-INV-20TimelineContractsPreparedPayload { ContractType: string }
RosreestrRegisteredТЗ.md:407TimelineEmptyPayload { }
DealCompletedDeal.CompleteTimelineEmptyPayload { }
DealCancelledDeal.CancelTimelineCancelledPayload { Reason: string }
SelectionSentагент отправил подборку в рамках сделки (ТЗ.md:447)TimelineSelectionPayload { SelectionId: Guid, SelectionTitle: string }
TermsAcknowledgedинвестор отметил «Ознакомлен» (US-INV-18)TimelineEmptyPayload { }
ManualNoteручная запись Менеджера ПИН / Службы заботы (US-MGR)TimelineManualNotePayload { Text: string (varchar 2000) }

Value objects

Value objects не используются (типизация — через per-type jsonb payload).

Связи

СвязьТипКлючПравило загрузкиDelete behavior
DealEntityreference (родитель)DealIdвыборка по DealId с сортировкой OccurredAt asc (лента)cascade
DealDocumentEntity / BookingEntityлогическая (id в payload)Payload.DocumentId / Payload.BookingIddisplay-ссылки UI

Инварианты и переходы состояний

ИнвариантУсловиеКогда проверяетсяОшибка
INV-TLE-1Append-only: контент события (Type,OccurredAt,Payload,ActorUserId) иммутабелен; меняется только Status/CompletedAt/UpdatedAtпо построению (контент не редактируется)
INV-TLE-2Запись — в одном согласованном изменении с породившей мутациейпри записи строки таймлайна
INV-TLE-3InProgress требует DueAt либо явного закрытия парным событием (например DocumentRequested закрывается DocumentUploaded)при добавлении строки таймлайнаCRM_TIMELINE_DUE_REQUIRED
INV-TLE-4Overdue ставит только worker timeline-overdue при Status = InProgress and DueAt < now; идемпотентно (ADR-0056)фоновая задача timeline-overdue
INV-TLE-5ManualNote создают только PinManager/Служба заботы (арбитраж)при создании заметкиCRM_TIMELINE_MANUAL_FORBIDDEN

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
TimelineItemAddedдоменноедобавление строкиDealId, EventId, Type — триггер push/email «новое событие в таймлайне» (US-INV-11) через notifications
Нет собственных Kafka-событий

Таймлайн не публикует собственных интеграционных событий: их порождают сами операции-источники (deal.md, booking.md, deal-document.md) — таймлайн лишь их локальная материализация. Уведомление US-INV-11 доставляет notifications как консюмер тех же событий.

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK; ix_deal_timeline_deal_occurred (deal_id,occurred_at); (deal_id,type); partial (due_at) where status = 'InProgress' (worker); (tenant_id)
Партиционирование / retentionnone (десятки строк на сделку; ADR-0056). Пересмотр — при реальном росте
Concurrencyнет (append-only + однопоточный worker overdue)
Permissions / PIIвидимость = видимость сделки (record-level deal.md); crm.deals.view. PII в payload запрещён: эскроу — только маска, документы — только id/тип/заголовок.
Auditсама лента — аудит бизнес-процесса; технический audit — на агрегатах-источниках

Материализация, порядок и восстановление

Материализация. Таймлайн — локальная проекция: строка пишется в одном согласованном изменении с породившей мутацией агрегата (создание сделки/брони/документа, смена этапа/статуса, оплата и т.п., INV-TLE-2). Собственных Kafka-событий таймлайн не публикует; интеграционные события порождают сами операции-источники, а уведомление US-INV-11 доставляет консюмер notifications тех же событий. Таким образом, для пользователя строка таймлайна появляется раньше или одновременно с внешними эффектами (карточка в чате, push): данные crm фиксируются атомарно, downstream — eventual.

Порядок ленты. Сортировка — по OccurredAt источника (ix_deal_timeline_deal_occurred), не по времени записи; поэтому событие, доставленное консюмером с задержкой, встаёт на свою историческую позицию, а не в конец.

Парные события и просрочка. Целевое действие пишется как InProgress с DueAt либо закрывается парным событием (DocumentRequestedDocumentUploaded), INV-TLE-3. Worker timeline-overdue при Status = InProgress and DueAt < now переводит строку в Overdue (только он, INV-TLE-4); проход идемпотентен (ADR-0056) — повторная обработка не меняет уже Overdue/Done строку и не плодит уведомлений.

Отказы, идемпотентность и конкуренция

  • Append-only иммутабельность. Контент строки (Type/OccurredAt/Payload/ActorUserId) не редактируется (INV-TLE-1); меняются только Status/CompletedAt/UpdatedAt. Дублей нет, т.к. запись идёт атомарно с единственной мутацией-источником.
  • Конкуренция. Практически отсутствует: лента append-only, Overdue ставит однопоточный worker. Закрытие InProgress парным событием и параллельная простановка Overdue разрешаются по статусу — как только строка Done, worker её не трогает.
  • PII-guard. В Payload персональные данные запрещены: эскроу — только маска ****NNNN, документы — только id/тип/заголовок; нарушение недопустимо по построению payload-моделей.
  • Ручные заметки. ManualNote создают только PinManager/Служба заботы (INV-TLE-5) → иначе CRM_TIMELINE_MANUAL_FORBIDDEN.

Acceptance-критерии

  • Happy (факт-событие). Дано: сделка создана. Когда фиксируется мутация. Тогда в ленте строка DealCreated со Status = Done, видимая всем участникам сделки, в том же согласованном изменении.
  • Целевое действие и просрочка. Дано: DocumentRequested с DueAt в прошлом, не закрыт. Когда работает worker timeline-overdue. Тогда строка Overdue; повторный проход ничего не меняет.
  • Закрытие парным событием. Дано: DocumentRequested InProgress. Когда приходит DocumentUploaded. Тогда исходная строка → Done (CompletedAt).
  • Отсутствие DueAt у целевого действия. Дано: InProgress-строка без DueAt и без парного закрытия. Тогда отказ CRM_TIMELINE_DUE_REQUIRED.
  • Ручная заметка чужим актором. Дано: не PinManager/Служба заботы. Когда создаёт ManualNote. Тогда отказ CRM_TIMELINE_MANUAL_FORBIDDEN.
  • Порядок при поздней доставке. Дано: событие с ранним OccurredAt доставлено позже. Когда оно записано. Тогда в ленте оно стоит по OccurredAt, а не в конце.

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

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

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

  • deal.md — источники событий и статусная модель; booking.md, deal-document.md — порождающие операции; lead.md — аналогичная лента лида.
  • Worker timeline-overdue../background/timeline-overdue-job.md (будущий путь по конвенции).