Calendar Service
Назначение
calendar — центр расписания платформы PIN: рабочий календарь агента и консультанта ПИН (показы,
консультации, встречи, задачи, дедлайны броней), слоты доступности для самозаписи инвесторов с витрины и
записи на встречу с полным жизненным циклом (подтверждение, отмена, перенос, исход). Календарь владеет
только расписанием: лиды и сделки остаются в crm (узнаёт о записях по событиям), доставка напоминаний — в
notifications, видеосвязь — в messenger. Контекст введён
ADR-0064.
Metadata
| Поле | Значение |
|---|---|
| Статус | draft |
| Owning team | PIN Platform (backend) |
| Domain | Расписание и запись на встречи (примыкает к домену продаж, см. service-boundaries.md) |
| Data classification | Internal (заметки/комментарии могут содержать персональные данные клиента) + Public (витрина свободных слотов) |
| Runtime criticality | medium (недоступность не блокирует сделки и брони; страдают самозапись и напоминания) |
Ownership
| Artifact | Owner/Rule |
|---|---|
| PostgreSQL schema | calendar |
| API areas | public (витрина слотов, самозапись), investor (мои записи), agent (события, слоты, решения по записям), agency (календарь команды, read-only), pin-manager (календари консультантов ПИН) |
| Owned entities | CalendarEventEntity (+ 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 events | 8 топиков pin.calendar.*.v1 — таблица в «Events И Consumers» |
| Consumed events | pin.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 indexes | none (выборки календаря — SQL-диапазоны по индексам, ADR-0056) |
| Analytics facts | none |
Ответственность И Границы
- Что сервис делает: ведение календарных событий владельца (создание/изменение/перенос/статусы,
soft-conflict-детекция пересечений); batch-публикация слотов доступности (DST-корректная генерация из
локальных окон); самозапись инвестора/анонима на слот с атомарной связкой «слот + запись + событие»;
жизненный цикл записи (подтверждение/отклонение агентом, отмена/перенос инвестором в пределах cutoff,
исход
Completed/NoShow, таймаутPending); авто-события «дедлайн брони» из booking-событий CRM; материализация и диспетчеризация напоминаний (событиеreminder-due→ доставляетnotifications); календарь команды для руководителя агентства; календари консультантов ПИН. - Source of truth: календарные события, напоминания, слоты доступности, записи.
- Реплики/projections: участники сделок CRM (для авто-событий дедлайнов и отображения) и display-данные пользователей (ФИО для витрины слотов и списков) — производные, восстанавливаются реплеем событий.
Чем календарь не владеет (и не пишет чужие таблицы — интеграция только событиями, ADR-0064 п.4):
| Не делает | Владелец | Как связаны |
|---|---|---|
| Лиды, клиенты, сделки, брони | crm | CRM консюмит 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 |
| Видеозвонки, конференции, чаты | messenger | MeetingLink события — просто ссылка (URL), календарь звонки не создаёт и не проводит |
| Пользователи, аутентификация, permissions | identity | JWT/permissions; lazy-регистрация анонима при самозаписи — вызов identity; display-реплика из pin.identity.users.v1 |
| Объекты недвижимости и проекты | chessboard | ProjectId/PropertyObjectId — логические ссылки без реплики и валидации существования |
Зависимости
| Зависимость | Тип | Зачем Нужна | Failure mode |
|---|---|---|---|
PostgreSQL (schema calendar) | db | Все агрегаты, реплики, reminder-строки, outbox | Недоступность = отказ API и worker-ов; Kafka-события накапливаются, обработка догоняет |
| Kafka | queue | Consume crm/identity; produce pin.calendar.* | Lag → задержка авто-событий дедлайнов, лент CRM и уведомлений; данные календаря корректны |
identity | service (JWT + s2s + events) | Акторы, tenant, permissions; get-or-create инвестора по телефону при book-slot (lazy-регистрация анонима — как в create-consultation-request); display-реплика пользователей | identity недоступен → самозапись анонима отказывает (5xx, витрина слотов работает read-only); авторизованные операции без JWT-проверки невозможны; лаг событий → устаревшие ФИО в display-реплике |
profile | service (read) | Default-таймзона владельца календаря (Timezone профиля, IANA) — предзаполнение TimeZone события/слот-батча | Недоступность → предзаполнение пропускается, клиент указывает таймзону явно (деградация UX, операции не блокируются) |
crm | service (events) | Booking-события → авто-события BookingDeadline; deal-события → реплика участников сделки | Нет событий → дедлайны не появляются/не переносятся, реплика устаревает; ручной календарь и самозапись работают |
notifications | consumer наших событий | Доставка напоминаний и уведомлений о записях по каналам | Потеря не влияет на данные календаря; напоминание может не дойти (алерт по 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, офсеты напоминаний | calendar | domain/calendar-event.md |
EventReminderEntity | Owned-строка напоминания «за N минут до начала»: Pending / Sent / Skipped, пересоздание при переносе, polling-диспетчер | calendar | domain/event-reminder.md |
AvailabilitySlotEntity | Материализованный слот самозаписи: batch-генерация (окна × дни × длительность, DST-корректно), Available / Booked / Expired / Archived, гонка — условный переход Available → Booked | calendar | domain/availability-slot.md |
AppointmentEntity | Запись инвестора на слот: снапшот времени, Pending / Confirmed / Declined / Cancelled / Expired / Completed / NoShow, каскады на слот и событие, цепочка переносов | calendar | domain/appointment.md |
CrmDealReplica | Реплика «сделка → агенты» из pin.crm.deal-* — множество агентов + ExecutorAgentUserId (владелец календаря дедлайнов; цель переназначения открытых событий при смене исполнителя / уходе агента) | — (реплика crm) | consumers/crm-deal-events.md |
UserDisplayReplicaEntity | Display-реплика ФИО пользователей из 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 → 409APPOINTMENT_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, маскирование в логах):
| Topic | Key | Когда | Потребители | Контракт |
|---|---|---|---|---|
pin.calendar.event-created.v1 | EventId | Создано календарное событие (любой Origin) | notifications | events/event-created.md |
pin.calendar.event-rescheduled.v1 | EventId | Перенос события (StartAt/EndAt изменены) | notifications | events/event-rescheduled.md |
pin.calendar.event-status-changed.v1 | EventId | Каждый переход статуса события | notifications | events/event-status-changed.md |
pin.calendar.event-owner-changed.v1 | EventId | Переназначение владельца открытого события при смене исполнителя / уходе агента сделки (INV-CAL-8) | notifications | events/event-owner-changed.md |
pin.calendar.appointment-created.v1 | AppointmentId | Создана запись (Pending/Confirmed) | crm (get-or-create клиента/лида + лента), notifications | events/appointment-created.md |
pin.calendar.appointment-confirmed.v1 | AppointmentId | Запись подтверждена (агентом или auto-confirm) | crm (лента лида), notifications | events/appointment-confirmed.md |
pin.calendar.appointment-cancelled.v1 | AppointmentId | Declined / Cancelled / Expired (Reason{DeclinedByAgent, CancelledByInvestor, CancelledByAgent, Expired}) | crm (лента лида), notifications | events/appointment-cancelled.md |
pin.calendar.appointment-completed.v1 | AppointmentId | Исход встречи (Outcome{Completed, NoShow}) | crm (лента лида), notifications | events/appointment-completed.md |
pin.calendar.reminder-due.v1 | EventId | Наступило напоминание (диспетчер, Pending → Sent) | notifications (AppointmentReminder / CalendarReminder) | events/reminder-due.md |
Потребляемые топики:
| Type | Topic | Назначение | Consumer-док |
|---|---|---|---|
| Consumed | pin.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 |
| Consumed | 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 | Авто-события BookingDeadline (Origin = CrmBooking): создание из ReserveExpiresAt/BookingExpiresAt, перенос при продлении, отмена при завершении/отмене брони; business-идемпотентность — unique (BookingId, фаза брони) | consumers/crm-booking-events.md; контракты — booking-created, booking-confirmed, booking-paid, booking-expired, booking-cancelled |
| Consumed | pin.identity.users.v1 | Display-реплика ФИО агентов/консультантов/инвесторов (витрина слотов, списки записей) | consumers/identity-user-upserted.md; контракт — user-upserted |
Сквозные Потоки (интеграционные последовательности)
- Самозапись с витрины (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. - Дедлайн брони (async).
crmпубликуетbooking-created→ консюмер календаря создаёт событиеBookingDeadline(Origin = CrmBooking,StartAt= дедлайн резерва/брони) в календаре агента сделки (владелец — из реплики участников) → напоминания уходят штатным механизмом; продление брони переносит событие, завершение/отмена — отменяет. - Напоминание (async). Диспетчер (цикл 60 с) переводит наступившие reminder-строки
Pending → Sentи публикуетreminder-due→notificationsдоставляет по каналам получателя. Точность — до ~1 цикла. - Два входа воронки. Слотовая самозапись (
book-slot— «выбираю конкретное время») и no-slot заявка create-consultation-request («время обсудим») сосуществуют и сходятся в CRM в одном клиенте/лиде (get-or-create по телефону на стороне CRM-консюмера).
Runtime И Эксплуатация
| Область | Требование |
|---|---|
| Health checks | /health/live, /health/ready (PostgreSQL + Kafka), /health/startup |
| Background workers | 3 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/SLA | API p95 < 300 мс; точность напоминаний ≤ 2 мин от DueAt; лаг consumer-ов crm/identity < 1 мин |
| Dashboard | Grafana calendar-overview (создаётся при внедрении) |
| Alerts | lag consumer-group > 5 мин; DLQ consumer-ов не пустой; отставание диспетчера напоминаний (Pending с DueAt старше 5 мин); рост зависших Pending-записей |
| Runbooks | runbooks/calendar-consumer-replay.md (по конвенции — повторная обработка booking/deal-событий) |
| Feature flags | none |
Безопасность
- Основные 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.
Связанные Документы
- technical-assignment.md (строка контекста
calendar), service-boundaries.md, glossary.md («Слот доступности», «Запись (Appointment)», «Дедлайн брони», «AutoConfirm», «Cutoff»). - Домен: domain/calendar-event.md, domain/event-reminder.md, domain/availability-slot.md, domain/appointment.md.
- Смежные контексты:
../crm/(лид-интейк, брони, сделки; no-slot путь — create-consultation-request),../notifications/(доставка),../identity/(permissions, lazy-регистрация, display-данные),../profile/(default-таймзона). - Сценарии: UC-OVR-001, UC-OVR-002, UC-OVR-003 — потоки «инвестор выбирает слот» и дедлайны брони в календаре.
- ADR: 0064 calendar-scheduling-context (границы, слоты, запись, напоминания, таймзоны, вне-scope), 0052 pin-tenancy-model (инвестор вне tenant), 0056 simplicity-first (PostgreSQL-only, без Redis, polling-worker-ы).
- Шаблон: service-readme-template.md.