Entity / Aggregate: AppointmentEntity
Дом полей агрегата «Запись» контекста calendar — договорённость инвестора и агента о встрече в конкретном
слоте (availability-slot.md). Запись — отдельный агрегат, а не поле слота и не вид
события (ADR-0064, п.4): у неё свой
жизненный цикл (подтверждение агентом, отмена, исход встречи), который каскадно управляет слотом и
порождённым календарным событием (calendar-event.md).
Назначение
AppointmentEntity создаётся, когда инвестор (или аноним через lazy-регистрацию по телефону — как в
create-consultation-request) бронирует слот агента
(../api/public/book-slot.md). Создание атомарно: переход слота Available → Booked + запись + календарное
событие (Planned, Origin = Appointment). Дальше запись живёт своей статус-машиной; CRM узнаёт о ней
только по событиям pin.calendar.appointment-* (get-or-create клиента/лида + лента лида — без
синхронных записей чужих таблиц).
Классификация
| Поле | Значение |
|---|---|
| Контекст | calendar |
| Kind | AggregateRoot |
| Source of truth | calendar |
| Таблица | calendar.appointments |
| Tenant isolation | TenantId — компания агента; инвестор — вне tenant (ADR-0052), доступ к своим записям — record-level по InvestorUserId |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK; Guid v7 | app | Идентификатор записи. |
TenantId | Guid | Да | index ix_appointments_tenant_id | из слота | Компания агента. |
SlotId | Guid | Да | FK fk_appointments_slot_id → calendar.availability_slots(id); unique partial ux_appointments_active_slot (SlotId) where status in ('Pending','Confirmed') | app | Забронированный слот. Не более одной активной записи на слот (INV-APT-1). |
AgentUserId | Guid | Да | index ix_appointments_agent_start (AgentUserId, StartAt); unique partial ux_appointments_active_pair (InvestorUserId, AgentUserId, Kind) where status in ('Pending','Confirmed') | снапшот из слота | Агент/консультант. |
InvestorUserId | Guid | Да | index partial ix_appointments_investor_active (InvestorUserId) where status in ('Pending','Confirmed') | app (identity; lazy-регистрация анонима) | Инвестор (пользователь identity, вне tenant). |
Kind | EventKind | Да | enum (хранится как text), varchar(32); только Showing/Consultation | снапшот из слота | Вид встречи (подмножество EventKind). |
StartAt | DateTimeOffset | Да | — | снапшот времени слота | Начало встречи, UTC; иммутабелен (INV-APT-5). |
EndAt | DateTimeOffset | Да | check EndAt > StartAt | снапшот времени слота | Конец встречи, UTC; иммутабелен. |
TimeZone | string | Да | varchar(64), IANA | снапшот из слота | Таймзона слота на момент бронирования. |
ProjectId | Guid? | Нет | — | снапшот из слота | Проект показа/консультации — логическая ссылка chessboard без валидации; иммутабельный снапшот значения слота на момент бронирования (INV-APT-5); попадает в payload appointment-created для ленты лида CRM. |
PropertyObjectId | Guid? | Нет | — | снапшот из слота | Объект недвижимости — логическая ссылка chessboard без валидации; иммутабельный снапшот значения слота на момент бронирования (INV-APT-5); попадает в payload appointment-created. |
Status | AppointmentStatus | Да | enum (хранится как text), varchar(32), index ix_appointments_status | app / worker | См. enum и полную матрицу переходов. |
Comment | string? | Нет | varchar(1024); класс данных Internal — маскирование в логах | app (инвестор при записи) | Комментарий инвестора («интересует 2-комнатная…»); попадает в payload appointment-created — нужен ленте лида CRM. |
PendingExpiresAt | DateTimeOffset? | Условно | index partial ix_appointments_pending_expires_at where status = 'Pending' | app: min(CreatedAt + 24 ч, StartAt − MinLeadTimeMinutes слота) | Дедлайн решения агента по Pending-записи (worker ../background/appointment-timeout.md); null при auto-confirm. |
ConfirmedAt | DateTimeOffset? | Нет | — | app | Момент подтверждения (→ Confirmed), в т.ч. auto-confirm. |
CancelReason | string? | Условно | varchar(512); обязателен при Declined/Cancelled | app | Причина отклонения/отмены. |
CancelledBy | AppointmentCancelledBy? | Условно | enum (хранится как text), varchar(32); обязателен при Status = Cancelled (check) | app / worker | Кто отменил (INV-APT-6). |
RescheduledFromAppointmentId | Guid? | Нет | FK self | app | Предыдущая запись, из которой выполнен перенос (у новой записи). |
RescheduledToAppointmentId | Guid? | Нет | FK self | app | Новая запись, в которую выполнен перенос (у отменённой записи). Ссылки пары взаимны, заполняются атомарно (INV-APT-5). |
CalendarEventId | Guid | Да | unique ux_appointments_calendar_event_id | app | Порождённое календарное событие (Origin = Appointment, связь 1:1). |
CompletedAt | DateTimeOffset? | Нет | — | app | Момент фиксации исхода (→ Completed / → NoShow). |
CreatedAt | DateTimeOffset | Да | index ix_appointments_created_at desc | app | Создание записи (= бронирование слота). |
UpdatedAt | DateTimeOffset | Да | — | app / worker | Последнее изменение. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший (null — worker). |
Version | int | Да | оптимистическая блокировка | app | Версия строки — арбитраж «агент ↔ инвестор ↔ worker». |
Enum AppointmentStatus
| Значение | Описание |
|---|---|
Pending = 1 | Ожидает решения агента (слот с AutoConfirm = false); действует PendingExpiresAt. |
Confirmed = 2 | Подтверждена (агентом или auto-confirm); встреча состоится. |
Declined = 3 | Отклонена агентом на этапе Pending. Терминальный. |
Cancelled = 4 | Отменена (инвестором до cutoff / агентом / системой при переносе). Терминальный. |
Expired = 5 | Агент не принял решение до PendingExpiresAt (worker). Терминальный. |
Completed = 6 | Встреча состоялась (исход зафиксирован агентом). Терминальный. |
NoShow = 7 | Клиент не пришёл (исход зафиксирован агентом). Терминальный. |
Активные статусы — Pending, Confirmed; остальные терминальны.
Enum AppointmentCancelledBy
| Значение | Описание |
|---|---|
Investor = 1 | Инвестор отменил/перенёс свою запись (в пределах cutoff). |
Agent = 2 | Агент отменил подтверждённую или ожидающую запись. |
System = 3 | Системная отмена платформой (административное вмешательство); переносы и таймауты сюда не относятся: перенос инвестором — Investor, таймаут Pending — отдельный статус Expired. |
Связи
| Связь | Тип | Ключ | Delete behavior |
|---|---|---|---|
AvailabilitySlotEntity | reference (запись занимает слот; параметры AutoConfirm/cutoff/lead-time читаются из слота-источника) | SlotId | restrict |
CalendarEventEntity | reference 1:1 (порождённое событие, Origin = Appointment) | CalendarEventId | restrict |
AppointmentEntity (перенос) | self-reference (цепочка переносов) | RescheduledFromAppointmentId / RescheduledToAppointmentId | restrict |
| Пользователи (identity) | внешняя логическая | AgentUserId / InvestorUserId | — (display-данные — из реплики identity) |
| Клиент/лид CRM | внешняя событийная | события pin.calendar.appointment-* → get-or-create на стороне CRM | — (календарь таблицы CRM не пишет) |
Полная матрица переходов
| Из | В | Кто / операция | Условия (guards) |
|---|---|---|---|
[*] | Pending | инвестор/аноним — ../api/public/book-slot.md | слот Available, AutoConfirm = false; INV-SLT-3/5; INV-APT-1/2; атомарно со слотом и событием |
[*] | Confirmed | инвестор/аноним — book-slot | слот Available, AutoConfirm = true; те же guard-ы; ConfirmedAt = now |
Pending | Confirmed | агент — ../api/agent/confirm-appointment.md (calendar.appointments.decide) | до PendingExpiresAt |
Pending | Declined | агент — ../api/agent/decline-appointment.md | CancelReason обязателен |
Pending | Cancelled | инвестор — ../api/investor/cancel-my-appointment.md | now ≤ StartAt − CancellationCutoffMinutes (INV-APT-3); CancelledBy = Investor |
Pending | Cancelled | агент — ../api/agent/cancel-appointment.md | CancelledBy = Agent, CancelReason обязателен |
Pending | Expired | worker ../background/appointment-timeout.md | now > PendingExpiresAt; идемпотентно |
Confirmed | Cancelled | инвестор — cancel-my-appointment / перенос | now ≤ StartAt − CancellationCutoffMinutes (INV-APT-3); CancelledBy = Investor (в т.ч. при переносе) |
Confirmed | Cancelled | агент — cancel-appointment | до фиксации исхода; CancelledBy = Agent, CancelReason обязателен |
Confirmed | Completed | агент — ../api/agent/complete-appointment.md (Outcome = Completed) | now ≥ StartAt (INV-APT-4) |
Confirmed | NoShow | агент — complete-appointment (Outcome = NoShow) | now ≥ StartAt (INV-APT-4) |
Любой другой переход → APPOINTMENT_INVALID_STATE. Перенос инвестором
(../api/investor/reschedule-my-appointment.md) — не переход, а атомарная пара: отмена текущей записи
(→ Cancelled) + создание новой на выбранный слот с взаимными ссылками RescheduledFrom/To (INV-APT-5);
guard-ы обеих половин действуют одновременно (cutoff старой + доступность нового слота), при отказе любой
половины не происходит ничего.
Каскады: переход записи → эффект на слот и календарное событие
Каскад выполняется в одном согласованном изменении с переходом записи — это единственный канал
управления слотом в Booked (INV-SLT-4) и событием Origin = Appointment (INV-CAL-3). Правило освобождения
слота: Booked → Available, если слот ещё пригоден для повторного бронирования
(now ≤ StartAt − MinLeadTimeMinutes), иначе Booked → Expired.
| Переход записи | Слот (availability-slot.md) | Календарное событие (calendar-event.md) | Интеграционное событие |
|---|---|---|---|
[*] → Pending (book-slot) | Available → Booked | создаётся Planned (Origin = Appointment, время/вид/место из слота) | appointment-created |
[*] → Confirmed (auto-confirm) | Available → Booked | создаётся Planned | appointment-created + appointment-confirmed |
Pending → Confirmed | остаётся Booked | остаётся Planned | appointment-confirmed |
Pending → Declined | Booked → Available / Expired | → Cancelled (CancelReason из записи) | appointment-cancelled (Reason = DeclinedByAgent) |
Pending → Cancelled (инвестор) | Booked → Available / Expired | → Cancelled | appointment-cancelled (Reason = CancelledByInvestor) |
Pending → Cancelled (агент) | Booked → Available / Expired | → Cancelled | appointment-cancelled (Reason = CancelledByAgent) |
Pending → Expired (worker) | Booked → Available / Expired | → Cancelled | appointment-cancelled (Reason = Expired) |
Confirmed → Cancelled (инвестор/агент/перенос) | Booked → Available / Expired | → Cancelled | appointment-cancelled (Reason = CancelledByInvestor / CancelledByAgent) |
Confirmed → Completed | Booked → Archived | → Completed | appointment-completed (Outcome = Completed) |
Confirmed → NoShow | Booked → Archived | → Completed (факт встречи зафиксирован; исход — на записи) | appointment-completed (Outcome = NoShow) |
Инварианты
| Инвариант | Условие | Когда проверяется | Ошибка |
|---|---|---|---|
| INV-APT-1 | На слот — не более одной активной записи (Pending/Confirmed); создание записи атомарно с условным переходом слота Available → Booked и созданием календарного события | бронирование, перенос (unique partial ux_appointments_active_slot + INV-SLT-3) | SLOT_ALREADY_BOOKED |
| INV-APT-2 | Антиспам: не более 1 активной записи на пару (InvestorUserId, AgentUserId, Kind) и не более 3 активных записей на инвестора суммарно | бронирование, перенос (unique partial ux_appointments_active_pair + подсчёт по ix_appointments_investor_active) | APPOINTMENT_DUPLICATE |
| INV-APT-3 | Cutoff инвестора: отмена/перенос инвестором — только пока now ≤ StartAt − CancellationCutoffMinutes (параметр слота); агент ограничением не связан | cancel-my-appointment, reschedule-my-appointment | APPOINTMENT_CUTOFF_PASSED |
| INV-APT-4 | Исход (Completed/NoShow) фиксирует только агент, только из Confirmed и только после наступления StartAt | complete-appointment | APPOINTMENT_NOT_STARTED / APPOINTMENT_INVALID_STATE |
| INV-APT-5 | Снапшот слота (StartAt/EndAt/TimeZone/Kind/ProjectId/PropertyObjectId) фиксируется при создании и неизменяем (последующее изменение слота-источника на запись не влияет); «перенос» = атомарная пара «отмена старой + новая запись на новый слот», ссылки RescheduledFrom/To взаимны и заполняются в одном согласованном изменении | все операции; перенос | APPOINTMENT_INVALID_STATE |
| INV-APT-6 | Терминальные статусы иммутабельны; каскады на слот и событие (таблица выше) выполняются в одном согласованном изменении с переходом записи; CancelledBy обязателен при Cancelled; Expired выставляет только worker | все операции над записью | APPOINTMENT_INVALID_STATE |
Ошибки
| Код | HTTP | Условие |
|---|---|---|
APPOINTMENT_NOT_FOUND | 404 | Запись не найдена или недоступна актору (record-level: инвестор видит только свои; агент — свои). |
APPOINTMENT_ACCESS_DENIED | 403 | Актор — не участник записи и не имеет calendar.appointments.view/calendar.team.view в tenant. |
APPOINTMENT_DUPLICATE | 409 | Нарушен антиспам INV-APT-2: уже есть активная запись на пару инвестор+агент+вид, либо у инвестора уже 3 активных записи. |
APPOINTMENT_CUTOFF_PASSED | 409 | Отмена/перенос инвестором позже StartAt − CancellationCutoffMinutes (INV-APT-3). |
APPOINTMENT_INVALID_STATE | 409 | Переход вне матрицы (в т.ч. операция над терминальной записью). |
APPOINTMENT_NOT_STARTED | 409 | Фиксация исхода до наступления StartAt (INV-APT-4). |
SLOT_ALREADY_BOOKED | 409 | Гонка за слот при создании/переносе — контракт слота (availability-slot.md). |
Полные таблицы ошибок бронирования (включая lead-time, rate limit анонима, lazy-регистрацию) — в
../api/public/book-slot.md.
Доменные и интеграционные события
Ключ партиционирования — AppointmentId (порядок событий одной записи гарантирован). Payload — без PII;
исключение — Comment инвестора в appointment-created (класс Internal, маскирование в логах): он нужен
CRM для ленты лида.
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
AppointmentCreatedEvent | интеграционное, topic pin.calendar.appointment-created.v1, ContractVersion = 1 | создание записи ([*] → Pending / → Confirmed) | AppointmentId, TenantId, SlotId, AgentUserId, InvestorUserId, Kind, StartAt, EndAt, TimeZone, Status, Comment? (Internal), ProjectId?, PropertyObjectId? (иммутабельный снапшот слота из полей записи, не перечитывается из слота на момент публикации), CalendarEventId, RescheduledFromAppointmentId?; консюмеры: crm (get-or-create клиента/лида + лента), notifications (AppointmentCreated); контракт — ../events/appointment-created.md |
AppointmentConfirmedEvent | интеграционное, topic pin.calendar.appointment-confirmed.v1, ContractVersion = 1 | → Confirmed (включая auto-confirm) | AppointmentId, TenantId, AgentUserId, InvestorUserId, Kind, StartAt, TimeZone, ConfirmedAt, AutoConfirmed (bool); консюмеры: crm (лента лида), notifications (AppointmentConfirmed); контракт — ../events/appointment-confirmed.md |
AppointmentCancelledEvent | интеграционное, topic pin.calendar.appointment-cancelled.v1, ContractVersion = 1 | → Declined / → Cancelled / → Expired | AppointmentId, TenantId, AgentUserId, InvestorUserId, Kind, StartAt, TimeZone, Reason (DeclinedByAgent / CancelledByInvestor / CancelledByAgent / Expired), CancelledBy?, RescheduledToAppointmentId?; консюмеры: crm (лента лида), notifications (AppointmentCancelled); контракт — ../events/appointment-cancelled.md |
AppointmentCompletedEvent | интеграционное, topic pin.calendar.appointment-completed.v1, ContractVersion = 1 | → Completed / → NoShow | AppointmentId, TenantId, AgentUserId, InvestorUserId, Kind, StartAt, TimeZone, Outcome (Completed / NoShow), CompletedAt; консюмеры: crm (лента лида), notifications; контракт — ../events/appointment-completed.md |
Интеграционные события публикуются атомарно со сменой состояния агрегата.
Межсервисные последовательности и промежуточные состояния
- Самозапись с витрины. Аноним выбирает слот → lazy-регистрация по телефону (identity, как в
create-consultation-request) → в одном согласованном
изменении: слот
Booked, записьPending/Confirmed, событиеPlanned, материализованы напоминания, исходящееappointment-created. Далее асинхронно:crmделает get-or-create клиента/лида и пишет ленту;notificationsуведомляет агента и инвестора. - Окно eventual-consistency. Запись в календаре агента видна сразу (данные
calendar); лид/лента в CRM пополняются после обработки события (обычно < 1 мин, ADR-0064). Это штатно: источник истины записи —calendar, CRM — downstream-проекция воронки. - Двойной вход воронки. Слотовая самозапись и no-slot заявка
(create-consultation-request) сосуществуют: первая —
«выбираю конкретное время», вторая — «время обсудим»; обе сходятся в CRM в одном клиенте/лиде
(get-or-create по телефону на стороне CRM-консюмера
../../crm/consumers/calendar-appointments.md).
Отказы, идемпотентность и конкуренция
- Гонка за слот при создании/переносе — ровно один победитель (INV-SLT-3); проигравший получает
SLOT_ALREADY_BOOKED, состояние записи/слота консистентно (либо всё, либо ничего). - Гонка «агент ↔ инвестор ↔ worker» (confirm vs cancel vs timeout) — арбитраж оптимистической
блокировкой (
Version): победитель фиксирует переход и каскад, проигравший перечитывает и получаетAPPOINTMENT_INVALID_STATE(или видит уже терминальный статус). - Идемпотентность worker-а
appointment-timeout: повторный проход над уже не-Pendingзаписью — no-op; ровно одноappointment-cancelled (Reason = Expired)на запись. - Повтор
book-slotтем же инвестором на тот же слот/того же агента — отклоняется структурно (ux_appointments_active_slot,ux_appointments_active_pair): дублей записи не возникает. - Частичный отказ downstream (
crm/notificationsнедоступны) — запись и календарь корректны; события доставляются at-least-once, консюмеры идемпотентны поAppointmentId— расхождение сходится повтором доставки.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK; ix_appointments_tenant_id; unique partial ux_appointments_active_slot (SlotId) where active; unique partial ux_appointments_active_pair (InvestorUserId, AgentUserId, Kind) where active; partial ix_appointments_investor_active (InvestorUserId) where active — лимит ≤ 3 и «мои записи»; ix_appointments_agent_start (AgentUserId, StartAt); ix_appointments_status; partial ix_appointments_pending_expires_at (PendingExpiresAt) where status = 'Pending' — worker; unique ux_appointments_calendar_event_id; ix_appointments_created_at desc |
| Партиционирование / retention | none (ADR-0056) |
| Concurrency | оптимистическая блокировка (Version) + построчная блокировка на связке со слотом |
| Permissions / PII | агент: calendar.appointments.view / calendar.appointments.decide (confirm/decline/cancel/complete — только свои записи); руководитель: calendar.team.view (read-only, свой tenant); инвестор: record-level по InvestorUserId (../api/investor/list-my-appointments.md, cancel-my-appointment.md, reschedule-my-appointment.md) — без permission-модели tenant-а. Comment — класс Internal: маскирование в логах, не отдаётся в публичной витрине; телефона в записи нет (контакты — в identity) |
| Audit | CreatedAt/ConfirmedAt/CompletedAt/UpdatedAt + UpdatedByUserId; CancelledBy/CancelReason — след отмены; цепочка переносов — RescheduledFrom/To; полная история — события pin.calendar.appointment-* |
Acceptance-критерии
- Самозапись (auto-confirm). Дано: слот
AvailableсAutoConfirm = true, лимиты инвестора не исчерпаны. Когда инвестор бронирует. Тогда атомарно: слотBooked, записьConfirmed, событиеPlanned(Origin = Appointment), опубликованыappointment-createdиappointment-confirmed. - Самозапись (ручное подтверждение). Дано: слот с
AutoConfirm = false. Когда инвестор бронирует. Тогда записьPendingсPendingExpiresAt, агент получает уведомление; послеconfirm-appointment—Confirmed. - Дубль на пару. Дано: у инвестора активная запись
Consultationк агенту A. Когда он бронирует второй слотConsultationтого же агента. Тогда отказAPPOINTMENT_DUPLICATE. - Лимит 3. Дано: у инвестора 3 активных записи (к разным агентам). Когда он бронирует четвёртую. Тогда
отказ
APPOINTMENT_DUPLICATE. - Cutoff. Дано:
Confirmed-запись,CancellationCutoffMinutes = 120, доStartAt60 минут. Когда инвестор отменяет. Тогда отказAPPOINTMENT_CUTOFF_PASSED; агент при этом отменить может. - Timeout. Дано:
Pending-запись,now > PendingExpiresAt. Когда проходит worker. Тогда записьExpired, слот освобождён (Available, либоExpired, если lead time уже прошёл), событиеCancelled, опубликованоappointment-cancelled (Reason = Expired); повторный проход — no-op. - Отклонение агентом. Дано:
Pending-запись. Когда агент отклоняет с причиной. Тогда записьDeclined, слот освобождён, событиеCancelled, опубликованоappointment-cancelled (Reason = DeclinedByAgent). - Исход раньше времени. Дано:
Confirmed-запись,now < StartAt. Когда агент фиксируетNoShow. Тогда отказAPPOINTMENT_NOT_STARTED. - Исход. Дано:
Confirmed-запись,now ≥ StartAt. Когда агент фиксируетOutcome = NoShow. Тогда записьNoShow, слотArchived, событиеCompleted, опубликованоappointment-completed (Outcome = NoShow). - Перенос. Дано:
Confirmed-запись в пределах cutoff и свободный слот того же агента. Когда инвестор переносит. Тогда атомарно: старая записьCancelled(RescheduledToAppointmentIdзаполнен), старый слот освобождён, новая запись создана на новый слот (RescheduledFromAppointmentIdзаполнен) со статусом поAutoConfirmнового слота; при занятости нового слота —SLOT_ALREADY_BOOKED, старая запись не тронута.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: AvailabilitySlotEntity, Entity / Aggregate: CalendarEventEntity
- API (21): GET /api/v1/agency/calendar/appointments — Записи команды (read-only), GET /api/v1/agent/calendar/appointments — Список записей к агенту, GET /api/v1/agent/calendar/appointments/{appointmentId} — Карточка записи, GET /api/v1/agent/calendar/events/{id} — Карточка события календаря, GET /api/v1/agent/calendar/slots — Список слотов доступности агента, GET /api/v1/investor/calendar/appointments — Мои записи на встречи, GET /api/v1/investor/calendar/appointments/{appointmentId} — Карточка моей записи, GET /api/v1/pin-manager/calendar/appointments — Записи к консультантам ПИН (read-only), POST /api/public/calendar/appointments — Самозапись на слот (бронирование), POST /api/v1/agent/calendar/appointments/{appointmentId}/cancel — Отмена записи агентом, POST /api/v1/agent/calendar/appointments/{appointmentId}/complete — Фиксация исхода встречи, POST /api/v1/agent/calendar/appointments/{appointmentId}/confirm — Подтверждение записи (+9)
- Use Cases (7): UC-CAL-003. Инвестор бронирует слот с витрины, UC-CAL-004. Агент подтверждает или отклоняет запись, UC-CAL-005. Отмена и перенос записи, UC-CAL-006. Агент фиксирует исход встречи, UC-CAL-009. Руководитель смотрит календарь команды, UC-CAL-010. Руководитель ПИН смотрит календари консультантов, US-реестр контекста calendar (US-CAL)
- События (8): Event Contract: pin.calendar.appointment-cancelled (AppointmentCancelledEvent), Event Contract: pin.calendar.appointment-completed (AppointmentCompletedEvent), Event Contract: pin.calendar.appointment-confirmed (AppointmentConfirmedEvent), Event Contract: pin.calendar.appointment-created (AppointmentCreatedEvent), Event Contract: pin.calendar.event-created (CalendarEventCreatedEvent), Event Contract: pin.calendar.event-rescheduled (CalendarEventRescheduledEvent), Event Contract: pin.calendar.event-status-changed (CalendarEventStatusChangedEvent), Event Contract: pin.calendar.reminder-due (ReminderDueEvent)
- Консюмеры (2): Consumer: Calendar Identity User Sync (calendar-identity-users), Consumer: CRM Calendar Appointments Sync (crm-calendar-appointments)
- Фоновые задачи (2): Background Job: AppointmentTimeoutJob, Background Job: SlotExpirationJob
- index.md (1): Calendar Service
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- availability-slot.md — слот и гонка бронирования; calendar-event.md — порождённое событие (INV-CAL-3); event-reminder.md — напоминания о встрече.
- ADR-0064 п.4 — запись как отдельный агрегат, событийная интеграция с CRM; ADR-0052 — инвестор вне tenant.
- CRM-сторона: create-consultation-request (no-slot
путь, lazy-регистрация, rate limit), consumer записей —
../../crm/consumers/calendar-appointments.md. - Контракты API:
../api/public/book-slot.md,../api/agent/{confirm,decline,cancel,complete}-appointment.md,../api/agent/list-appointments.md,../api/investor/{list-my-appointments,get-my-appointment,cancel-my-appointment,reschedule-my-appointment}.md,../api/agency/list-team-appointments.md,../api/pin-manager/list-appointments.md. - Worker:
../background/appointment-timeout.md; события:../events/appointment-{created,confirmed,cancelled,completed}.md.