Entity / Aggregate: ChatEventEntity
Дом полей сущности «Событие ленты чата» контекста messenger — системная лента структурных фактов
жизненного цикла чата (создан / переименован / участник вошёл-вышел / роль изменена / закрыт / архивирован).
Все проекции (системная строка в UI, шапка/состав чата) ссылаются сюда и описывают только дельту.
Назначение
ChatEventEntity — append-only лента лёгких системных фактов чата. В составе агрегата
ChatEntity (messenger — source of truth, ADR-0055: свой мини-мессенджер). Записывается
в той же транзакции, что и породившая мутация (создание чата, изменение состава). Строки различаются
дискриминатором Type; типизированные данные — versioned jsonb Payload (модели ниже). Рендерится тонкой
системной строкой.
Отличие от EventCardMessage: ChatEvent — лёгкие системные факты жизненного цикла чата
БЕЗ read-receipts / правок / персональных текстов, НЕ участвует в Seq/unread (отдельная лента,
INV-CE-3); EventCardMessage — богатые бизнес-карточки сделки в ленте сообщений (участвуют в unread,
имеют статусы прочтения). Состав/статус чата — chat.md, роли — crm.
Классификация
| Поле | Значение |
|---|---|
| Контекст | messenger |
| Kind | Entity (append-only, в составе агрегата ChatEntity; отдельная таблица) |
| Source of truth | messenger |
| Таблица | messenger.chat_events |
| Tenant isolation | Нет собственной колонки: изоляция через агрегат Chat (членство); tenancy — ADR-0052 |
Поля (базовая часть)
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор события. |
ChatId | Guid | Да | FK fk_chat_events_chat_id → messenger.chats(id) cascade, index ix_chat_events_chat_occurred (ChatId,OccurredAt) | app / consumer | Чат (chat.md). |
Type | ChatEventType | Да | дискриминатор, enum (text), varchar(48), index (chat_id,type) | app | Тип события (см. enum). |
OccurredAt | DateTimeOffset | Да | часть индекса ленты | app / доменное событие | Когда факт произошёл (сортировка ленты «от старых к новым»). |
ActorUserId | Guid? | Нет | логическая ссылка на identity.users(id) | app / consumer | Кто совершил действие; null — система / консюмер. |
Payload | typed jsonb per-type | Да | jsonb; модели ниже; ContractVersion = 1 | app | Типизированные данные события. |
CreatedAt | DateTimeOffset | Да | — | app | Запись строки. |
Enum ChatEventType и typed payload (jsonb)
Все payload-модели — ContractVersion = 1; правило совместимости — additive-only, breaking → новый
ContractVersion. Поля с ? — nullable, остальные обязательны; enum — строкой. Role в членских событиях —
значения ParticipantRole для чата сделки (дом — crm deal.md); Kind — тип чата
(enum ChatKind, дом — chat.md).
| Тип | Когда пишется (источник) | Payload-модель |
|---|---|---|
ChatCreated | создание чата (домен Chat) | ChatCreatedPayload { Kind: string, DealId: Guid? } |
DealChatProvisioned | консюмер pin.crm.deal-created.v1 создал чат сделки | DealChatProvisionedPayload { DealId: Guid } |
Renamed | Chat.Rename | ChatRenamedPayload { OldTitle: string, NewTitle: string } |
ParticipantJoined | участник добавлен (консюмер / домен) | ParticipantJoinedPayload { UserId: Guid, Role: string } |
ParticipantLeft | участник удалён (консюмер / домен) | ParticipantLeftPayload { UserId: Guid, Role: string } |
ParticipantRoleChanged | смена роли участника | ParticipantRoleChangedPayload { UserId: Guid, FromRole: string, ToRole: string } |
ChatClosed | чат закрыт (терминальный статус сделки — Completed/Cancelled) | ChatClosedPayload { Reason: string } |
ChatArchived | чат архивирован | ChatEmptyPayload { } |
Каждая модель — versioned (ContractVersion = 1); эволюция схемы — только добавление nullable-полей
(новые Since v2 без bump топика — топика у ленты нет; сериализуется в колонку payload jsonb).
Value objects
Value objects не используются (типизация — через per-type jsonb payload).
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
ChatEntity | reference (родитель) | ChatId | лента с пагинацией по ChatId, сортировка OccurredAt asc | cascade |
DealEntity (crm) | логическая (id в payload) | Payload.DealId | display-ссылка UI | — |
identity.users | логическая (id) | ActorUserId / Payload.UserId | display-имя из реплики pin.identity.users.v1 | — |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-CE-1 | Append-only: контент (Type,OccurredAt,Payload,ActorUserId) иммутабелен — не редактируется и не удаляется | отсутствие изменения/удаления контента | — |
| INV-CE-2 | Запись — в одной транзакции с породившей мутацией участника/чата | операции контекста messenger / доменные подписчики | — |
| INV-CE-3 | Не влияет на Seq/unread: отдельная лента, системная строка не инкрементит непрочитанное сообщений | конфигурация ленты; Seq живёт на message.md | — |
| INV-CE-4 | Членские события (ParticipantJoined/Left/RoleChanged) для чата сделки пишет консюмер pin.crm.deal-members-changed.v1 идемпотентно — без дублей при set-реконсиляции | консюмер (де-дуп по сообщению + version-guard) | — |
Собственного статуса у сущности нет; ниже — жизненный цикл агрегата Chat, который лента лишь
фиксирует (append-only ledger переходов, статус — на chat.md):
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
ChatEventAppended | доменное | добавление строки ленты | ChatId, EventId, Type — триггер push в SignalR-hub ChatUpdated (обновить шапку/состав) |
Собственных Kafka-событий лента не публикует: это материализация доменных событий чата/участников.
Интеграционный контракт готовности чата (pin.messenger.deal-chat-status-changed.v1) принадлежит агрегату
Chat, а не ленте. Real-time доставка ChatUpdated — по SignalR-паттерну SDK.
Порядок и eventual-доставка. Строки ленты упорядочены OccurredAt («от старых к новым»); при нескольких
фактах в одной мутации (напр. set-реконсиляция состава из deal-members-changed) все пишутся в одной
транзакции — порядок стабилен. Членские события идемпотентны (INV-CE-4): повтор доставки CRM-события не плодит
ParticipantJoined/Left/RoleChanged. Real-time ChatUpdated — best-effort надстройка над persist: строка
ленты авторитетна, при пропущенном push клиент восстановит шапку/состав при следующем чтении.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK; ix_chat_events_chat_occurred (chat_id,occurred_at); (chat_id,type) |
| Партиционирование / retention | none (десятки строк на чат; ADR-0056). Пересмотр — при реальном росте |
| Concurrency | нет (append-only — read-modify-write отсутствует) |
| Permissions / PII | видимость = членство в чате (chat.md). PII нет: только UserId/роли (строкой), отображаемые имена резолвятся из реплики pin.identity.users.v1; заголовки в Renamed — не PII |
| Audit | сама лента — аудит жизненного цикла чата; технический audit — на агрегате Chat |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity: ParticipantEntity (owned-child ChatEntity)
- API (1): POST /api/messenger/chats/direct — Открыть/создать личный 1:1 чат
- Use Cases (2): UC-MSG-002. Авто-провизия чата сделки по событию CRM, UC-MSG-008. Добавленный участник появляется в чате сделки/лида
- Консюмеры (4): Consumer: Deal Chat Members Sync (MessengerDealMembersChangedConsumer), Consumer: Deal Chat Provisioning (MessengerDealCreatedConsumer), Consumer: Deal Stage Card (MessengerDealStageChangedConsumer), Consumer: Lead Chat Participants Sync (MessengerLeadParticipantsChangedConsumer)
- Фоновые задачи (2): Background Job: ChatProvisionRetryJob, Background Job: RealtimeDispatchJob
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- Агрегат-родитель (дом статуса/состава/
ChatKind): chat.md; богатые карточки ленты сообщений и их отличие: message.md. - Источники членских/провизионных событий (crm): deal.md (
ParticipantRole), deal-members-changed.md, deal-created.md. - Решения: ADR-0055 (свой мини-мессенджер), ADR-0056 (simplicity-first), ADR-0052 (tenancy).