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

POST /api/v1/agent/calendar/events — Создание события календаря

Назначение

Владелец календаря (агент агентства или консультант ПИН) вручную создаёт событие своего рабочего календаря: показ, консультацию, встречу или задачу. Событие создаётся в статусе Planned с Origin = Manual, по офсетам напоминаний материализуются reminder-строки, публикуется pin.calendar.event-created.v1. Пересечение с другими событиями владельца создание не блокирует — ответ несёт конфликт-warning (HasConflicts = true, INV-CAL-6).

Автособытия BookingDeadline через эту операцию не создаются — их порождает только consumer booking-событий CRM (../../consumers/crm-booking-events.md); события записей порождает бронирование слота (../public/book-slot.md).

Metadata

ПолеЗначение
Сервис/контекстcalendar
API areaagent
Feature groupcalendar-events
Статусdraft
Документdocs/06-services/calendar/api/agent/create-event.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (агент агентства / консультант ПИН — владелец календаря)
Auth policy / Permissionscalendar.events.manage
Scope (RBAC)организация: компания актора (агентство / ПИН)
Record-levelсобытие создаётся только в собственном календаре: OwnerUserId = ActorUserId из токена (sub), не из body — создать событие другому агенту нельзя
Tenant isolationTenantId из токена (tenant_id)
Auditcreate: CreatedByUserId = ActorUserId — иммутабельный след происхождения (вместе с Origin = Manual)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/agent/calendar/events
Success status201
Idempotency headerIdempotency-Key (опционально; повтор с тем же ключом возвращает первый результат, дубль события не создаётся)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdтокен (sub)GuidДаRequiredстановится OwnerUserId и CreatedByUserId
TenantIdтокен (tenant_id)GuidДаRequiredTenant isolation
KindbodyEventKindДа{Showing, Consultation, Meeting, Task}; значение вне enum → ValidationError; BookingDeadlineCAL_EVENT_KIND_FIELDS_MISMATCH
StartAtbodyDateTimeOffsetДаUTC; прошлое допустимо (фиксация задним числом — напоминания с прошедшим DueAt материализуются как Skipped)
EndAtbodyDateTimeOffset?УсловноUTC; EndAt > StartAt; обязателен для Showing/Consultation/Meeting; null (точечное событие) — только Task (INV-CAL-1)
TimeZonebodystring?НетIANA-идентификатор, ≤ 64 символа; при отсутствии — Timezone профиля владельца (profile.md); не определилась ни из body, ни из профиля / не валидный IANA → CAL_EVENT_TIMEZONE_UNKNOWN
Titlebodystring?Нет≤ 256 символовПДн клиента: в логи не пишется
Descriptionbodystring?Нет≤ 8192 символаПДн клиента: в логи не пишется
Locationbodystring?Нет≤ 512 символов, свободный текстПДн клиента: в логи не пишется
MeetingLinkbodystring?Условно≤ 256 символов, абсолютный URL; допустим только для Consultation/Meeting (INV-CAL-5)
PrioritybodyEventPriority?Нетenum Low/Medium/High/Critical; default Medium
ClientUserIdbodyGuid?Нетлогическая ссылка на пользователя identity; существование не валидируется
DealIdbodyGuid?Нетлогическая ссылка на сделку CRM
LeadIdbodyGuid?Нетлогическая ссылка на лид CRM
PropertyObjectIdbodyGuid?Нетлогическая ссылка на объект chessboard
ProjectIdbodyGuid?Нетлогическая ссылка на проект chessboard
ReminderOffsetsMinutesbodyint[]?Нетпри отсутствии — default {1440, 60}; явный пустой массив = без напоминаний; ≤ 5 элементов, каждый в 5..40320, без дублей — иначе CAL_EVENT_REMINDER_OFFSETS_INVALID

BookingId в модели запроса нет: поле заполняет только consumer booking-событий (Origin = CrmBooking).

Модель ответа — CalendarEventModel

201 с созданным событием — единичная CalendarEventModel (дом модели и правило HasConflictslist-events.md). Ключевые значения: Status = Planned, Origin = Manual, OwnerUserId = ActorUserId, Version = 1; HasConflicts вычислен на момент создания — это и есть конфликт-warning для клиента.

Алгоритм

  1. Контекст и доступ. Актор — владелец календаря; tenant — его компания; требуется calendar.events.manage. OwnerUserId/TenantId — из токена.
  2. Проверка входа. Kind ∈ допустимого подмножества: значение вне enum → ValidationError; Kind = BookingDeadlineCAL_EVENT_KIND_FIELDS_MISMATCH (вручную не создаётся). Длины Title/Description/Location/MeetingLink, формат URL, enum Priority — нарушение → ValidationError. Офсеты напоминаний в ограничениях (≤ 5, диапазон 5..40320, уникальны) — иначе CAL_EVENT_REMINDER_OFFSETS_INVALID.
  3. Таймзона. Если TimeZone не передана — взять Timezone профиля владельца; при недоступности профиля или пустой таймзоне, а также при не валидном IANA-идентификаторе → CAL_EVENT_TIMEZONE_UNKNOWN.
  4. Интервал и kind-матрица. INV-CAL-1: EndAt > StartAt; EndAt = null допустим только для Task — иначе CAL_EVENT_INVALID_INTERVAL. INV-CAL-5: MeetingLink присутствует при Kind ∈ {Showing, Task}CAL_EVENT_KIND_FIELDS_MISMATCH.
  5. Создание. В одном согласованном изменении: событие Planned (Origin = Manual, CreatedByUserId = ActorUserId, Priority default Medium); материализация reminder-строк по офсетам (правило 1 event-reminder.md: DueAt = StartAt − Offset; прошедший DueAt → строка сразу Skipped); публикация pin.calendar.event-created.v1.
  6. Конфликт-warning. Пересечение с активными (Planned/InProgress) интервальными событиями владельца не блокирует создание (INV-CAL-6) — в ответе HasConflicts = true; решение (перенести/оставить) — за владельцем.
  7. Результат. 201 + CalendarEventModel созданного события.

Побочные эффекты

ТипДетали
Интеграционные событияpin.calendar.event-created.v1, key = EventId, атомарно с созданием (контракт — event-created.md)
Уведомленияdownstream: notifications консюмит event-created (../../../notifications/consumers/calendar-events.md)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Kind вне enum; длины/формат полей; enum Priority вне значений; StartAt отсутствует«Некорректные данные события»no retry
CAL_EVENT_INVALID_INTERVAL400EndAt ≤ StartAt; EndAt = null при Kind ∈ {Showing, Consultation, Meeting} (INV-CAL-1)«Некорректный интервал события»no retry
CAL_EVENT_KIND_FIELDS_MISMATCH400Kind = BookingDeadline; MeetingLink при Showing/Task (INV-CAL-5)«Поля не соответствуют виду события»no retry
CAL_EVENT_TIMEZONE_UNKNOWN400TimeZone не валидный IANA-идентификатор либо не передан и default профиля недоступен/пуст«Не удалось определить таймзону»no retry
CAL_EVENT_REMINDER_OFFSETS_INVALID400офсеты вне ограничений: > 5 элементов, значение вне 5..40320, дубли«Некорректные офсеты напоминаний»no retry
UNAUTHORIZED401нет/просрочен JWTno retry
FORBIDDEN403нет права calendar.events.manageno retry

Предусловия и постусловия

  • Предусловия. Актор авторизован, имеет calendar.events.manage. Профиль владельца может содержать default-таймзону (иначе TimeZone обязателен в запросе — деградация без блокировки, недоступность profile создание с явной таймзоной не ломает).
  • Постусловия. Создано ровно одно событие Planned (Origin = Manual); материализованы reminder-строки по актуальным офсетам (прошедшие — Skipped); опубликовано ровно одно pin.calendar.event-created.v1. Никакие другие события владельца не изменены (soft-conflict ничего не блокирует и не двигает).

Acceptance-критерии

  • Happy path. Дано: агент с calendar.events.manage. Когда: POST Consultation с корректным интервалом, MeetingLink и офсетами по умолчанию. Тогда: 201, событие Planned/Origin = Manual, две Pending-reminder-строки (1440, 60), опубликовано event-created.
  • Конфликт-warning. Дано: у владельца активное событие 14:00–15:00. Когда: создаётся Meeting 14:30–15:30. Тогда: 201 (не блокируется), в ответе HasConflicts = true.
  • BookingDeadline вручную. Дано: Kind = BookingDeadline. Тогда: 400 CAL_EVENT_KIND_FIELDS_MISMATCH.
  • MeetingLink у показа. Дано: Kind = Showing с MeetingLink. Тогда: 400 CAL_EVENT_KIND_FIELDS_MISMATCH.
  • Точечное не-Task. Дано: Kind = Showing, EndAt = null. Тогда: 400 CAL_EVENT_INVALID_INTERVAL.
  • Таймзона из профиля. Дано: TimeZone не передана, в профиле владельца Europe/Moscow. Тогда: 201, TimeZone = Europe/Moscow. Если и профиль пуст — 400 CAL_EVENT_TIMEZONE_UNKNOWN.
  • Событие задним числом. Дано: StartAt — вчера. Тогда: 201; reminder-строки созданы сразу в Skipped, reminder-due не публикуется.
  • Без напоминаний. Дано: ReminderOffsetsMinutes = []. Тогда: 201, reminder-строк нет.
  • Повтор с Idempotency-Key. Дано: повторный POST с тем же ключом (ретрай по таймауту). Тогда: тот же 201 с первым результатом; второе событие и второе event-created не появляются.

Совместимость и наблюдаемость

  • Новые optional поля запроса/ответа — additive (без breaking change); неизвестное enum-значение в body → ValidationError. Контракт event-createdContractVersion = 1, additive-эволюция.
  • Logs: TenantId, OwnerUserId, EventId, Kind, HasConflicts (без Title/Description/Location); metrics: calendar_api_request_duration_seconds{route}, счётчик созданий по Kind; traces — сквозной traceparent.

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

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

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