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

Entity / Aggregate: CalendarEventEntity

Дом полей агрегата «Календарное событие» контекста calendar — единая плоская модель всех видов событий рабочего календаря агента (показ, консультация, встреча, задача, дедлайн брони). Вид события — enum Kind, а не тип-наследник (ADR-0064, п.2: TPH-иерархия черновика отброшена). Все проекции (модели ответов календаря, payload событий pin.calendar.event-*) ссылаются сюда и описывают только дельту.

Назначение

CalendarEventEntity — элемент рабочего календаря владельца (агента, консультанта ПИН): что, когда и с кем происходит. Событие появляется тремя путями (Origin): агент создаёт его вручную; запись инвестора на слот порождает событие автоматически (appointment.md); booking-события CRM порождают автособытия «дедлайн брони» (consumer ../consumers/crm-booking-events.md). Из офсетов события материализуются напоминания (event-reminder.md), которые доставляет контекст notifications.

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

ПолеЗначение
Контекстcalendar
KindAggregateRoot
Source of truthcalendar
Таблицаcalendar.calendar_events
Tenant isolationTenantId — компания владельца календаря (агентство агента / компания ПИН для консультанта), ADR-0052

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPK; Guid v7 (упорядочен по времени)appИдентификатор события.
TenantIdGuidДаindex ix_calendar_events_tenant_idиз владельца календаряКомпания владельца (агентство / ПИН).
OwnerUserIdGuidДаindex ix_calendar_events_owner_start (OwnerUserId, StartAt)app / запись / consumerВладелец календаря — агент или консультант ПИН. Может переназначаться консюмером deal-событий CRM при смене исполнителя / уходе агента сделки (INV-CAL-8, ../consumers/crm-deal-events.md).
CreatedByUserIdGuid?НетappКто создал вручную; null для событий, порождённых записью или consumer-ом.
KindEventKindДаenum (хранится как text), varchar(32)app / запись / consumerВид события, см. enum ниже.
OriginEventOriginДаenum (хранится как text), varchar(32); иммутабеленapp / запись / consumerПроисхождение — определяет, кто управляет жизненным циклом (INV-CAL-3/4).
StatusEventStatusДаenum (хранится как text), varchar(32), index ix_calendar_events_statusapp / запись / consumerТекущий статус, см. stateDiagram.
PriorityEventPriorityДаenum (хранится как text), varchar(32), default MediumappПриоритет для сортировки/подсветки в календаре.
StartAtDateTimeOffsetДаindex (в составе ix_calendar_events_owner_start)app / слот / consumerНачало, UTC (ADR-0064 п.6).
EndAtDateTimeOffset?Условноcheck EndAt > StartAt (если задан)app / слот / consumerКонец, UTC. null = точечное событие — допустимо только для Task/BookingDeadline (матрица kind-обязательностей).
TimeZonestringДаvarchar(64), IANA (например Europe/Moscow)app / слот / consumerТаймзона, в которой событие имеет смысл; отображение локального времени — забота клиента.
Titlestring?Нетvarchar(256)app / запись / consumerЗаголовок. Для Origin ≠ Manual формируется источником автоматически; при отсутствии клиент отображает производный заголовок по Kind.
Descriptionstring?Нетvarchar(8192)appОписание/заметки владельца.
Locationstring?Нетvarchar(512)app / слотМесто встречи — свободный текст (без привязки к справочнику городов, ADR-0064 п.7).
MeetingLinkstring?Условноvarchar(256), абсолютный URLappСсылка на онлайн-встречу; допустима только для Consultation/Meeting (матрица).
ClientUserIdGuid?Нетindex partial where not nullapp / записьКлиент-инвестор (пользователь identity, вне tenant). Для Origin = Appointment заполняется записью.
AppointmentIdGuid?Условноunique partial ux_calendar_events_appointment_id where not nullзаписьПорождающая запись (appointment.md); обязателен и заполняется только при Origin = Appointment (связь 1:1).
DealIdGuid?Условноindex partial ix_calendar_events_deal_id where not nullapp / consumerСделка CRM (логическая ссылка); обязателен для BookingDeadline. По индексу консюмер deal-событий находит открытые события сделки для переназначения владельца (INV-CAL-8).
LeadIdGuid?Нетapp / consumerЛид CRM (логическая ссылка).
BookingIdGuid?Условноindex partial ix_calendar_events_booking_id where not nullconsumerБронь CRM (booking.md); обязателен для BookingDeadline.
PropertyObjectIdGuid?Нетapp / слот / consumerОбъект недвижимости (chessboard, логическая ссылка).
ProjectIdGuid?Нетapp / слотПроект застройщика (chessboard, логическая ссылка).
ReminderOffsetsMinutesint[]Даdefault {1440, 60}; ≤ 5 элементов; каждый в диапазоне 5..40320; значения уникальны; пустой массив = без напоминанийappОфсеты напоминаний «за N минут до StartAt» — источник материализации reminder-строк (event-reminder.md).
CancelReasonstring?Условноvarchar(512); обязателен при Status = Cancelled (check)app / запись / consumerПричина отмены.
CreatedAtDateTimeOffsetДаappСоздание.
UpdatedAtDateTimeOffsetДаappПоследнее изменение.
UpdatedByUserIdGuid?НетappПоследний изменивший (null — worker/consumer).
VersionintДаоптимистическая блокировкаappВерсия строки; гонка «агент ↔ worker/consumer» разрешается перечиткой проигравшим.

Enum EventKind

ЗначениеОписание
Showing = 1Показ объекта недвижимости клиенту.
Consultation = 2Консультация клиента (очная или онлайн).
Meeting = 3Прочая встреча (внутренняя, с застройщиком и т.п.).
Task = 4Задача владельца календаря (позвонить, подготовить документы).
BookingDeadline = 5Дедлайн резерва/брони из CRM — автособытие, порождается consumer-ом booking-событий.

Enum EventOrigin

ЗначениеОписание
Manual = 1Создано владельцем календаря вручную; полный ручной жизненный цикл.
Appointment = 2Порождено записью инвестора на слот; управляется только каскадами переходов записи (INV-CAL-3).
CrmBooking = 3Порождено booking-событием CRM; управляется только consumer-ом ../consumers/crm-booking-events.md (INV-CAL-4).

Enum EventStatus

ЗначениеОписание
Planned = 1Запланировано (начальный статус).
InProgress = 2В работе — владелец начал событие. Именно InProgress, а не Cancelled: баг черновика «start → Cancelled» исправлен (ADR-0064 п.2).
Completed = 3Завершено. Терминальный.
Cancelled = 4Отменено (CancelReason обязателен). Терминальный.

Enum EventPriority

ЗначениеОписание
Low = 1Низкий.
Medium = 2Обычный (default).
High = 3Высокий.
Critical = 4Критический (например, дедлайн брони с оплатой).

Матрица kind-условных обязательностей

Проверяется при создании и каждом изменении события (нарушение → CAL_EVENT_KIND_FIELDS_MISMATCH).

KindEndAtMeetingLinkBookingId + DealIdПримечание
Showingобязателензапрещёнопциональныобычно заполнены PropertyObjectId/ProjectId и ClientUserId.
Consultationобязателенопционаленопциональныонлайн-консультация — с MeetingLink.
Meetingобязателенопционаленопциональны
Taskопционален (null = точечная задача)запрещёнопциональны
BookingDeadlineопционален (обычно null — точечный дедлайн)запрещёнобязательны обасоздаёт только consumer (Origin = CrmBooking); StartAt = ReserveExpiresAt/BookingExpiresAt брони.

Связи

СвязьТипКлючDelete behavior
EventReminderEntityowned (часть агрегата)CalendarEventIdcascade (жизненный цикл — правила материализации, event-reminder.md)
AppointmentEntityreference 1:1 (при Origin = Appointment)AppointmentIdrestrict
Бронь CRM (crm)внешняя логическаяBookingId— (реплики нет; consumer правит событие по booking-событиям)
Сделка / лид CRM (crm)внешняя логическаяDealId / LeadId
Объект / проект (chessboard)внешняя логическаяPropertyObjectId / ProjectId

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

ИнвариантУсловиеКогда проверяетсяОшибка
INV-CAL-1Интервал корректен: EndAt > StartAt, если EndAt задан; EndAt = null (точечное событие) допустим только для Kind ∈ {Task, BookingDeadline}создание, изменение, переносCAL_EVENT_INVALID_INTERVAL
INV-CAL-2Переходы статуса — только по матрице stateDiagram; start переводит в InProgress (фикс бага черновика, ADR-0064 п.2)start/complete/cancel-операции, каскадыCAL_EVENT_INVALID_STATE
INV-CAL-3Событие Origin = Appointment изменяется/переносится/отменяется только каскадами переходов записи (таблица каскадов — appointment.md); прямые операции агента над таким событием отклоняютсявсе операции изменения событияCAL_EVENT_MANAGED_BY_APPOINTMENT
INV-CAL-4Событие Origin = CrmBooking изменяется только consumer-ами CRM: booking-событий (перенос дедлайна при продлении, отмена при завершении/отмене брони) и deal-событий (переназначение OwnerUserId при смене исполнителя, INV-CAL-8); прямые операции агента отклоняютсявсе операции изменения событияCAL_EVENT_MANAGED_BY_CRM_BOOKING
INV-CAL-5Kind-условные обязательности полей соблюдены (матрица выше): BookingDeadlineBookingId + DealId; MeetingLink — только Consultation/Meetingсоздание, изменениеCAL_EVENT_KIND_FIELDS_MISMATCH
INV-CAL-6Пересечение интервалов событий одного OwnerUserIdsoft-conflict: создание/перенос не блокируется, но выборки календаря возвращают признак HasConflicts = true по каждому пересекающемуся событию (Planned/InProgress)выборки календаря (../api/agent/list-events.md)— (не ошибка)
INV-CAL-7Терминальные статусы (Completed, Cancelled) иммутабельны; при переходе в Cancelled обязателен CancelReasonвсе операции изменения событияCAL_EVENT_ALREADY_TERMINAL / CAL_EVENT_CANCEL_REASON_REQUIRED
INV-CAL-8Санкционированное переназначение владельца. Консюмер deal-событий CRM (../consumers/crm-deal-events.md) переназначает OwnerUserId открытых (Planned/InProgress) deal-привязанных событий (Kind = BookingDeadline + Origin ∈ {CrmBooking, Manual} встречи/задачи с DealId) на нового исполнителя при смене исполнителя (deal-members-changed ExecutorChanged) или уходе агента (Removed/увольнение). Меняется только OwnerUserId (в пределах того же TenantId = AgencyCompanyId); Origin = Appointment не переназначается (INV-CAL-3); публикуется pin.calendar.event-owner-changed.v1переназначение консюмером deal-событий— (санкционированный автоматический канал)

Перенос (reschedule) — не переход статуса: StartAt/EndAt меняются у события в Planned/InProgress с сохранением длительности (контракт — ../api/agent/reschedule-event.md), напоминания пересоздаются (event-reminder.md), публикуется pin.calendar.event-rescheduled.v1.

Ошибки

КодHTTPУсловие
CAL_EVENT_NOT_FOUND404Событие не найдено или недоступно в tenant актора.
CAL_EVENT_ACCESS_DENIED403Актор — не владелец события и не имеет calendar.team.view (read-only) в своём tenant.
CAL_EVENT_INVALID_INTERVAL400EndAt ≤ StartAt, либо EndAt отсутствует у интервального Kind (INV-CAL-1).
CAL_EVENT_KIND_FIELDS_MISMATCH400Нарушена матрица kind-обязательностей (INV-CAL-5).
CAL_EVENT_TIMEZONE_UNKNOWN400TimeZone — не валидный IANA-идентификатор.
CAL_EVENT_REMINDER_OFFSETS_INVALID400Офсеты напоминаний вне ограничений (количество/диапазон/дубли).
CAL_EVENT_CANCEL_REASON_REQUIRED400Отмена без CancelReason (INV-CAL-7).
CAL_EVENT_INVALID_STATE409Переход вне матрицы статусов (INV-CAL-2).
CAL_EVENT_ALREADY_TERMINAL409Операция над Completed/Cancelled событием (INV-CAL-7).
CAL_EVENT_MANAGED_BY_APPOINTMENT409Прямая операция над событием Origin = Appointment (INV-CAL-3); управлять нужно записью.
CAL_EVENT_MANAGED_BY_CRM_BOOKING409Прямая операция над событием Origin = CrmBooking (INV-CAL-4).

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

Payload всех событий pin.calendar.*без PII (только идентификаторы, вид/статус/время); ключ партиционирования — EventId (порядок событий одного календарного события гарантирован).

СобытиеТипКогдаSnapshot/поля
CalendarEventCreatedEventинтеграционное, topic pin.calendar.event-created.v1, ContractVersion = 1создание события (любой Origin)EventId, TenantId, OwnerUserId, Kind, Origin, Priority, StartAt, EndAt?, TimeZone, ClientUserId?, AppointmentId?, DealId?, BookingId?; консюмер: notifications; контракт — ../events/event-created.md
CalendarEventRescheduledEventинтеграционное, topic pin.calendar.event-rescheduled.v1, ContractVersion = 1перенос (StartAt/EndAt изменены)EventId, TenantId, OwnerUserId, Kind, StartAt, EndAt? (новые значения); консюмер: notifications (CalendarEventRescheduled); контракт — ../events/event-rescheduled.md
CalendarEventStatusChangedEventинтеграционное, topic pin.calendar.event-status-changed.v1, ContractVersion = 1каждый переход статусаEventId, TenantId, OwnerUserId, Kind, Origin, Status (новый), CancelReason?, ClientUserId?, StartAt, TimeZone; консюмер: notifications; контракт — ../events/event-status-changed.md
CalendarEventOwnerChangedEventинтеграционное, topic pin.calendar.event-owner-changed.v1, ContractVersion = 1переназначение владельца открытого события консюмером deal-событий CRM (INV-CAL-8)EventId, TenantId, OwnerUserId (новый), PreviousOwnerUserId, Kind, Origin, StartAt, EndAt?, TimeZone, DealId?; консюмер: notifications; контракт — ../events/event-owner-changed.md

Интеграционные события публикуются атомарно со сменой состояния агрегата. Напоминания публикуют собственное событие pin.calendar.reminder-due.v1 — см. event-reminder.md.

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

АспектЗначение
ИндексыPK; ix_calendar_events_tenant_id; ix_calendar_events_owner_start (OwnerUserId, StartAt) — range-выборки календаря; ix_calendar_events_status; unique partial ux_calendar_events_appointment_id (AppointmentId) where not null — 1:1 с записью; partial ix_calendar_events_booking_id (BookingId) where not null — выборки consumer-а booking-событий; partial ix_calendar_events_deal_id (DealId) where not null — переназначение владельца открытых событий сделки consumer-ом deal-событий (INV-CAL-8)
Партиционирование / retentionnone (ADR-0056)
Concurrencyоптимистическая блокировка (Version): гонка «владелец ↔ worker/consumer» — проигравший перечитывает состояние
Permissions / PIIcalendar.events.view (владелец), calendar.events.manage (владелец — создание/изменение/статусы), calendar.team.view (руководитель агентства — read-only календарь команды в своём tenant, ../api/agency/list-team-events.md). Description/Location/Title могут содержать персональные данные клиента → в интеграционные события не попадают, в логах маскируются; ClientUserId — только идентификатор, display-данные — из реплики identity
Auditотметки создания/изменения (кто и когда); Origin/CreatedByUserId — иммутабельный след происхождения; история статусов — через pin.calendar.event-status-changed.v1

Acceptance-критерии

  • Создание вручную. Дано: агент с calendar.events.manage. Когда создаёт Consultation с корректным интервалом и MeetingLink. Тогда событие Planned (Origin = Manual), материализованы reminder-строки по офсетам, опубликован pin.calendar.event-created.v1.
  • Start → InProgress. Дано: событие Planned. Когда владелец начинает событие. Тогда статус InProgress (не Cancelled), опубликован pin.calendar.event-status-changed.v1.
  • Точечное событие не для Task. Дано: создание Showing без EndAt. Тогда отказ CAL_EVENT_INVALID_INTERVAL.
  • MeetingLink у показа. Дано: создание Showing с MeetingLink. Тогда отказ CAL_EVENT_KIND_FIELDS_MISMATCH.
  • Событие записи защищено. Дано: событие Origin = Appointment. Когда агент вызывает прямой cancel-event. Тогда отказ CAL_EVENT_MANAGED_BY_APPOINTMENT; отмена возможна только операциями записи.
  • Дедлайн брони защищён. Дано: событие Origin = CrmBooking. Когда агент редактирует его. Тогда отказ CAL_EVENT_MANAGED_BY_CRM_BOOKING; событие меняет только consumer booking-/deal-событий.
  • Переназначение владельца при смене исполнителя. Дано: открытый BookingDeadline и открытая встреча сделки у агента A. Когда consumer deal-событий обрабатывает смену исполнителя на B (INV-CAL-8). Тогда OwnerUserId обоих событий = B, TenantId не изменён, по каждому опубликован pin.calendar.event-owner-changed.v1; событие Origin = Appointment при этом остаётся у A.
  • Soft-conflict. Дано: два Planned-события владельца пересекаются. Когда агент запрашивает календарь. Тогда оба события возвращаются с HasConflicts = true; создание второго не блокировалось.
  • Отмена без причины. Дано: событие Planned. Когда отмена без CancelReason. Тогда отказ CAL_EVENT_CANCEL_REASON_REQUIRED.
  • Терминальная иммутабельность. Дано: событие Completed. Когда любой переход/правка. Тогда отказ CAL_EVENT_ALREADY_TERMINAL.

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

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

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

  • event-reminder.md — напоминания (owned); appointment.md — запись, порождающая событие; availability-slot.md — слот самозаписи.
  • ADR-0064 — границы контекста, маппинг черновика, фикс «start → InProgress», таймзоны; ADR-0052 — tenancy; ADR-0056 — polling-worker-ы.
  • Источник дедлайнов брони: booking.md, booking-created.md, booking-expired.md; consumer — ../consumers/crm-booking-events.md.
  • Контракты API: ../api/agent/list-events.md, create-event.md, update-event.md, reschedule-event.md, start-event.md, complete-event.md, cancel-event.md.