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

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
KindEntity (append-only, в составе агрегата ChatEntity; отдельная таблица)
Source of truthmessenger
Таблицаmessenger.chat_events
Tenant isolationНет собственной колонки: изоляция через агрегат Chat (членство); tenancy — ADR-0052

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

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор события.
ChatIdGuidДаFK fk_chat_events_chat_id → messenger.chats(id) cascade, index ix_chat_events_chat_occurred (ChatId,OccurredAt)app / consumerЧат (chat.md).
TypeChatEventTypeДадискриминатор, enum (text), varchar(48), index (chat_id,type)appТип события (см. enum).
OccurredAtDateTimeOffsetДачасть индекса лентыapp / доменное событиеКогда факт произошёл (сортировка ленты «от старых к новым»).
ActorUserIdGuid?Нетлогическая ссылка на identity.users(id)app / consumerКто совершил действие; null — система / консюмер.
Payloadtyped jsonb per-typeДаjsonb; модели ниже; ContractVersion = 1appТипизированные данные события.
CreatedAtDateTimeOffsetДа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 }
RenamedChat.RenameChatRenamedPayload { 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
ChatEntityreference (родитель)ChatIdлента с пагинацией по ChatId, сортировка OccurredAt asccascade
DealEntity (crm)логическая (id в payload)Payload.DealIddisplay-ссылка UI
identity.usersлогическая (id)ActorUserId / Payload.UserIddisplay-имя из реплики pin.identity.users.v1

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

ИнвариантУсловиеГде проверяетсяОшибка
INV-CE-1Append-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)
Партиционирование / retentionnone (десятки строк на чат; ADR-0056). Пересмотр — при реальном росте
Concurrencyнет (append-only — read-modify-write отсутствует)
Permissions / PIIвидимость = членство в чате (chat.md). PII нет: только UserId/роли (строкой), отображаемые имена резолвятся из реплики pin.identity.users.v1; заголовки в Renamed — не PII
Auditсама лента — аудит жизненного цикла чата; технический audit — на агрегате Chat

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

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

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