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

Calendar Service

Назначение

calendar — центр расписания платформы PIN: рабочий календарь агента и консультанта ПИН (показы, консультации, встречи, задачи, дедлайны броней), слоты доступности для самозаписи инвесторов с витрины и записи на встречу с полным жизненным циклом (подтверждение, отмена, перенос, исход). Календарь владеет только расписанием: лиды и сделки остаются в crm (узнаёт о записях по событиям), доставка напоминаний — в notifications, видеосвязь — в messenger. Контекст введён ADR-0064.

Metadata

ПолеЗначение
Статусdraft
Owning teamPIN Platform (backend)
DomainРасписание и запись на встречи (примыкает к домену продаж, см. service-boundaries.md)
Data classificationInternal (заметки/комментарии могут содержать персональные данные клиента) + Public (витрина свободных слотов)
Runtime criticalitymedium (недоступность не блокирует сделки и брони; страдают самозапись и напоминания)

Ownership

ArtifactOwner/Rule
PostgreSQL schemacalendar
API areaspublic (витрина слотов, самозапись), investor (мои записи), agent (события, слоты, решения по записям), agency (календарь команды, read-only), pin-manager (календари консультантов ПИН)
Owned entitiesCalendarEventEntity (+ owned EventReminderEntity), AvailabilitySlotEntity, AppointmentEntity
Реплики (не source of truth)сделки CRM (маршрутизация дедлайнов + переназначение владельца) — CrmDealReplica (calendar.crm_deal_replicas, множество агентов + маркер ExecutorAgentUserId, консюмер consumers/crm-deal-events.md); display-данные пользователей — UserDisplayReplicaEntity (calendar.user_display_replicas, консюмер consumers/identity-user-upserted.md)
Produced events8 топиков pin.calendar.*.v1 — таблица в «Events И Consumers»
Consumed eventspin.crm.deal-created.v1, pin.crm.deal-members-changed.v1, pin.crm.deal-completed.v1, pin.crm.deal-cancelled.v1, pin.crm.booking-created.v1, pin.crm.booking-confirmed.v1, pin.crm.booking-paid.v1, pin.crm.booking-expired.v1, pin.crm.booking-cancelled.v1, pin.identity.users.v1
Search indexesnone (выборки календаря — SQL-диапазоны по индексам, ADR-0056)
Analytics factsnone

Ответственность И Границы

  • Что сервис делает: ведение календарных событий владельца (создание/изменение/перенос/статусы, soft-conflict-детекция пересечений); batch-публикация слотов доступности (DST-корректная генерация из локальных окон); самозапись инвестора/анонима на слот с атомарной связкой «слот + запись + событие»; жизненный цикл записи (подтверждение/отклонение агентом, отмена/перенос инвестором в пределах cutoff, исход Completed/NoShow, таймаут Pending); авто-события «дедлайн брони» из booking-событий CRM; материализация и диспетчеризация напоминаний (событие reminder-due → доставляет notifications); календарь команды для руководителя агентства; календари консультантов ПИН.
  • Source of truth: календарные события, напоминания, слоты доступности, записи.
  • Реплики/projections: участники сделок CRM (для авто-событий дедлайнов и отображения) и display-данные пользователей (ФИО для витрины слотов и списков) — производные, восстанавливаются реплеем событий.

Чем календарь не владеет (и не пишет чужие таблицы — интеграция только событиями, ADR-0064 п.4):

Не делаетВладелецКак связаны
Лиды, клиенты, сделки, брониcrmCRM консюмит pin.calendar.appointment-* → get-or-create клиента/лида + лента лида (../crm/consumers/calendar-appointments.md); календарь консюмит pin.crm.deal-* / pin.crm.booking-*
Доставка уведомлений и напоминаний по каналамnotificationsконсюмит все pin.calendar.* (../notifications/consumers/calendar-events.md); календарь только публикует reminder-due
Видеозвонки, конференции, чатыmessengerMeetingLink события — просто ссылка (URL), календарь звонки не создаёт и не проводит
Пользователи, аутентификация, permissionsidentityJWT/permissions; lazy-регистрация анонима при самозаписи — вызов identity; display-реплика из pin.identity.users.v1
Объекты недвижимости и проектыchessboardProjectId/PropertyObjectId — логические ссылки без реплики и валидации существования

Зависимости

ЗависимостьТипЗачем НужнаFailure mode
PostgreSQL (schema calendar)dbВсе агрегаты, реплики, reminder-строки, outboxНедоступность = отказ API и worker-ов; Kafka-события накапливаются, обработка догоняет
KafkaqueueConsume crm/identity; produce pin.calendar.*Lag → задержка авто-событий дедлайнов, лент CRM и уведомлений; данные календаря корректны
identityservice (JWT + s2s + events)Акторы, tenant, permissions; get-or-create инвестора по телефону при book-slot (lazy-регистрация анонима — как в create-consultation-request); display-реплика пользователейidentity недоступен → самозапись анонима отказывает (5xx, витрина слотов работает read-only); авторизованные операции без JWT-проверки невозможны; лаг событий → устаревшие ФИО в display-реплике
profileservice (read)Default-таймзона владельца календаря (Timezone профиля, IANA) — предзаполнение TimeZone события/слот-батчаНедоступность → предзаполнение пропускается, клиент указывает таймзону явно (деградация UX, операции не блокируются)
crmservice (events)Booking-события → авто-события BookingDeadline; deal-события → реплика участников сделкиНет событий → дедлайны не появляются/не переносятся, реплика устаревает; ручной календарь и самозапись работают
notificationsconsumer наших событийДоставка напоминаний и уведомлений о записях по каналамПотеря не влияет на данные календаря; напоминание может не дойти (алерт по lag)

Проекты

src/services/calendar/
Pin.Calendar.Api/
Pin.Calendar.Application/
Pin.Calendar.Domain/
Pin.Calendar.Infrastructure/
Pin.Calendar.Background/
Pin.Calendar.Migrator/
Pin.Calendar.Contracts/

Основные Сущности

EntityНазначениеSource Of TruthДокумент
CalendarEventEntityСобытие календаря: Kind{Showing, Consultation, Meeting, Task, BookingDeadline}, Origin{Manual, Appointment, CrmBooking}, статусы Planned → InProgress → Completed / Cancelled, офсеты напоминанийcalendardomain/calendar-event.md
EventReminderEntityOwned-строка напоминания «за N минут до начала»: Pending / Sent / Skipped, пересоздание при переносе, polling-диспетчерcalendardomain/event-reminder.md
AvailabilitySlotEntityМатериализованный слот самозаписи: batch-генерация (окна × дни × длительность, DST-корректно), Available / Booked / Expired / Archived, гонка — условный переход Available → Bookedcalendardomain/availability-slot.md
AppointmentEntityЗапись инвестора на слот: снапшот времени, Pending / Confirmed / Declined / Cancelled / Expired / Completed / NoShow, каскады на слот и событие, цепочка переносовcalendardomain/appointment.md
CrmDealReplicaРеплика «сделка → агенты» из pin.crm.deal-* — множество агентов + ExecutorAgentUserId (владелец календаря дедлайнов; цель переназначения открытых событий при смене исполнителя / уходе агента)— (реплика crm)consumers/crm-deal-events.md
UserDisplayReplicaEntityDisplay-реплика ФИО пользователей из pin.identity.users.v1 — витрина слотов и списки записей— (реплика identity)consumers/identity-user-upserted.md

ER-обзор

API

Карта областей (детальные документы — api/ по api-method-template, пишутся следующим слоем ТЗ; пути — по конвенции):

  • Public (аноним, rate limit по IP+телефону): api/public/list-available-slots.md (витрина свободных слотов: фильтры агент/проект/объект + диапазон + Kind), api/public/book-slot.md (самозапись: lazy-регистрация по телефону через identity, атомарная связка слот+запись+событие);
  • Investor (record-level по InvestorUserId, вне tenant): api/investor/list-my-appointments.md, api/investor/get-my-appointment.md, api/investor/cancel-my-appointment.md (cutoff → 409 APPOINTMENT_CUTOFF_PASSED), api/investor/reschedule-my-appointment.md (атомарно: отмена + бронь нового слота);
  • Agent (calendar.events.view/manage, calendar.slots.manage, calendar.appointments.view/decide) — события: api/agent/list-events.md (диапазон from/to, признак HasConflicts), api/agent/get-event.md, api/agent/create-event.md, api/agent/update-event.md, api/agent/reschedule-event.md (длительность сохраняется), api/agent/start-event.md, api/agent/complete-event.md, api/agent/cancel-event.md; слоты: api/agent/publish-slots.md (batch ≤ 500, отчёт «создано/пропущено»), api/agent/list-slots.md, api/agent/archive-slot.md (Booked → 409); записи: api/agent/list-appointments.md, api/agent/get-appointment.md, api/agent/confirm-appointment.md, api/agent/decline-appointment.md, api/agent/cancel-appointment.md, api/agent/complete-appointment.md (Outcome = Completed | NoShow);
  • Agency (calendar.team.view, read-only, свой tenant): api/agency/list-team-events.md, api/agency/list-team-appointments.md;
  • Pin-manager (платформенный scope, календари консультантов ПИН — UC-OVR-002): api/pin-manager/list-events.md, api/pin-manager/list-appointments.md;
  • Internal: not applicable — синхронного internal-API нет, интеграция только событиями (ADR-0064 п.7).

Events И Consumers

Производимые топики (все — ContractVersion = 1, публикация атомарно со сменой состояния агрегата через транзакционный outbox; payload без PII — исключение: Comment инвестора в appointment-created, класс Internal, маскирование в логах):

TopicKeyКогдаПотребителиКонтракт
pin.calendar.event-created.v1EventIdСоздано календарное событие (любой Origin)notificationsevents/event-created.md
pin.calendar.event-rescheduled.v1EventIdПеренос события (StartAt/EndAt изменены)notificationsevents/event-rescheduled.md
pin.calendar.event-status-changed.v1EventIdКаждый переход статуса событияnotificationsevents/event-status-changed.md
pin.calendar.event-owner-changed.v1EventIdПереназначение владельца открытого события при смене исполнителя / уходе агента сделки (INV-CAL-8)notificationsevents/event-owner-changed.md
pin.calendar.appointment-created.v1AppointmentIdСоздана запись (Pending/Confirmed)crm (get-or-create клиента/лида + лента), notificationsevents/appointment-created.md
pin.calendar.appointment-confirmed.v1AppointmentIdЗапись подтверждена (агентом или auto-confirm)crm (лента лида), notificationsevents/appointment-confirmed.md
pin.calendar.appointment-cancelled.v1AppointmentIdDeclined / Cancelled / Expired (Reason{DeclinedByAgent, CancelledByInvestor, CancelledByAgent, Expired})crm (лента лида), notificationsevents/appointment-cancelled.md
pin.calendar.appointment-completed.v1AppointmentIdИсход встречи (Outcome{Completed, NoShow})crm (лента лида), notificationsevents/appointment-completed.md
pin.calendar.reminder-due.v1EventIdНаступило напоминание (диспетчер, Pending → Sent)notifications (AppointmentReminder / CalendarReminder)events/reminder-due.md

Потребляемые топики:

TypeTopicНазначениеConsumer-док
Consumedpin.crm.deal-created.v1, pin.crm.deal-members-changed.v1, pin.crm.deal-completed.v1, pin.crm.deal-cancelled.v1Реплика «сделка → участники»: владелец календаря для авто-событий дедлайнов; терминальные события закрывают репликуconsumers/crm-deal-events.md; контракты — deal-created, deal-members-changed, deal-completed, deal-cancelled
Consumedpin.crm.booking-created.v1, pin.crm.booking-confirmed.v1, pin.crm.booking-paid.v1, pin.crm.booking-expired.v1, pin.crm.booking-cancelled.v1Авто-события BookingDeadline (Origin = CrmBooking): создание из ReserveExpiresAt/BookingExpiresAt, перенос при продлении, отмена при завершении/отмене брони; business-идемпотентность — unique (BookingId, фаза брони)consumers/crm-booking-events.md; контракты — booking-created, booking-confirmed, booking-paid, booking-expired, booking-cancelled
Consumedpin.identity.users.v1Display-реплика ФИО агентов/консультантов/инвесторов (витрина слотов, списки записей)consumers/identity-user-upserted.md; контракт — user-upserted

Сквозные Потоки (интеграционные последовательности)

  1. Самозапись с витрины (sync + async). Аноним/инвестор выбирает слот → book-slot: lazy-регистрация по телефону (sync-вызов identity) → в одном согласованном изменении слот Available → Booked, запись Pending/Confirmed, событие Planned (Origin = Appointment), материализованы напоминания → appointment-created (+ appointment-confirmed при auto-confirm) → async: crm делает get-or-create клиента/лида и пишет ленту, notifications уведомляет агента и инвестора. Запись в календаре агента видна сразу; лид в CRM — после обработки события (обычно < 1 мин) — штатное окно eventual consistency.
  2. Дедлайн брони (async). crm публикует booking-created → консюмер календаря создаёт событие BookingDeadline (Origin = CrmBooking, StartAt = дедлайн резерва/брони) в календаре агента сделки (владелец — из реплики участников) → напоминания уходят штатным механизмом; продление брони переносит событие, завершение/отмена — отменяет.
  3. Напоминание (async). Диспетчер (цикл 60 с) переводит наступившие reminder-строки Pending → Sent и публикует reminder-duenotifications доставляет по каналам получателя. Точность — до ~1 цикла.
  4. Два входа воронки. Слотовая самозапись (book-slot — «выбираю конкретное время») и no-slot заявка create-consultation-request («время обсудим») сосуществуют и сходятся в CRM в одном клиенте/лиде (get-or-create по телефону на стороне CRM-консюмера).

Runtime И Эксплуатация

ОбластьТребование
Health checks/health/live, /health/ready (PostgreSQL + Kafka), /health/startup
Background workers3 polling-worker-а с построчной блокировкой (паттерн booking-expiration, ADR-0056): background/reminder-dispatch.md (цикл 60 с), background/appointment-timeout.md (Pending → Expired + освобождение слота), background/slot-expiration.md (Available → Expired по наступлении StartAt)
SLO/SLAAPI p95 < 300 мс; точность напоминаний ≤ 2 мин от DueAt; лаг consumer-ов crm/identity < 1 мин
DashboardGrafana calendar-overview (создаётся при внедрении)
Alertslag consumer-group > 5 мин; DLQ consumer-ов не пустой; отставание диспетчера напоминаний (Pending с DueAt старше 5 мин); рост зависших Pending-записей
Runbooksrunbooks/calendar-consumer-replay.md (по конвенции — повторная обработка booking/deal-событий)
Feature flagsnone

Безопасность

  • Основные actor types: агент/консультант ПИН (владелец календаря), инвестор (свои записи, вне tenant), аноним витрины (просмотр слотов + самозапись), руководитель агентства (календарь команды), менеджер ПИН, system-actor consumer-ов и worker-ов.
  • Permission model — 6 позиций calendar.* (каталог — permission.md): calendar.events.view, calendar.events.manage, calendar.slots.manage, calendar.appointments.view, calendar.appointments.decide, calendar.team.view. Инвестор действует record-level по InvestorUserId без permission-модели tenant-а; аноним — только публичная витрина.
  • Sensitive data classes: Title/Description/Location события и Comment записи могут содержать персональные данные клиента — в интеграционные события не попадают (исключение — Comment в appointment-created, класс Internal), в логах маскируются; витрина слотов PII не отдаёт.
  • Audit requirements: actor-stamping и версия (Version) на агрегатах; Origin/CreatedByUserId — иммутабельный след происхождения события; история статусов — интеграционные события; записи и события не удаляются (отмена — статус).
  • Tenant isolation rule (ADR-0052): календарь, слоты и записи — tenant компании агента (агентство / ПИН); инвестор — вне tenant (record-level); worker-ы и consumer-ы — system-scope.

Use-cases

UCНазваниеДокумент
UC-CAL-001Агент ведёт рабочий календарь (события, статусы, перенос, конфликты)use-cases/UC-CAL-001-agent-manages-calendar.md
UC-CAL-002Агент публикует слоты доступности (batch, DST, отчёт)use-cases/UC-CAL-002-agent-publishes-slots.md
UC-CAL-003Инвестор/аноним бронирует слот с витрины (lazy-регистрация, гонка)use-cases/UC-CAL-003-investor-books-slot.md
UC-CAL-004Агент подтверждает или отклоняет запись (Pending, таймаут)use-cases/UC-CAL-004-appointment-confirmation.md
UC-CAL-005Отмена и перенос записи (cutoff инвестора, атомарная пара переноса)use-cases/UC-CAL-005-appointment-cancellation-and-reschedule.md
UC-CAL-006Агент фиксирует исход встречи (Completed / NoShow)use-cases/UC-CAL-006-appointment-outcome.md
UC-CAL-007Дедлайны брони CRM в календаре агента (авто-события)use-cases/UC-CAL-007-booking-deadlines-in-calendar.md
UC-CAL-008Напоминания о событиях (материализация, диспетчер, доставка)use-cases/UC-CAL-008-reminders.md
UC-CAL-009Руководитель смотрит календарь команды (read-only, tenant)use-cases/UC-CAL-009-agency-head-views-team-calendar.md

Известные Ограничения И Assumptions

Вне scope v1 — осознанные, принятые ограничения (зафиксировано в ADR-0064, п.5/7; каждое — сознательное упрощение под целевую нагрузку ADR-0056, не пробел; у каждого пункта в ADR — триггер пересмотра):

  • Отсутствие recurrence (повторяющиеся события, RRULE) — принято: повторяющуюся доступность закрывает batch-генерация слотов, повторяющиеся встречи в v1 не нужны; слот и порождённое событие всегда одиночные.
  • Синхронный internal-API календаря — не вводится, интеграция с другими контекстами только событийная.
  • Переназначение события/записи другому агенту — не вводится (отмена + новая запись).
  • capacity = 1 — принято: групповые показы вне scope, один слот обслуживает ровно одного инвестора (структурно — не более одной активной записи на слот, INV-APT-1), поле вместимости не вводится.
  • Слот и событие без привязки к городу справочника handbook — принято: v1 использует свободный Location (текст) + опциональные ProjectId/PropertyObjectId (логические ссылки chessboard без реплики и валидации существования); справочник городов на слот/событие не заводится.

Assumptions:

  • Booking-события CRM несут дедлайны (ReserveExpiresAt/BookingExpiresAt) и DealId — требование к контрактам pin.crm.booking-* (booking-created.md).
  • Все моменты хранятся и передаются в UTC; TimeZone (IANA) на событии/слот-батче — для локального отображения и DST-корректной генерации; отдельного справочника таймзон нет (строка, ADR-0063).
  • Консюмеры downstream (crm, notifications) идемпотентны по AppointmentId/ReminderId — доставка at-least-once.

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