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

GET /api/v1/agent/calendar/events — Список событий календаря агента

Назначение

Отдаёт владельцу календаря (агенту агентства или консультанту ПИН) его события за обязательный диапазон дат: показы, консультации, встречи, задачи и автоматические дедлайны брони — с признаком пересечения HasConflicts по каждому событию. Это основная выборка календарной сетки (день/неделя/месяц) в кабинете агента. Документ — дом модели CalendarEventModel, правила выборки диапазона и правила вычисления HasConflicts: остальные выборки событий контекста ссылаются сюда.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (агент агентства / консультант ПИН — владелец календаря)
Auth policy / Permissionscalendar.events.view
Scope (RBAC)организация: компания актора (агентство / ПИН)
Record-levelOwnerUserId = ActorUserId из токена (sub), не из query — актор видит только собственные события; чужие события в выборку не попадают (fail-closed). Календарь команды — отдельная операция ../agency/list-team-events.md (calendar.team.view)
Tenant isolationTenantId из токена (tenant_id); изоляция применяется независимо от параметров запроса
Auditread: актор и фильтры фиксируются в trace; содержимое событий (Title/Description/Location — могут содержать ПДн клиента) в логи не пишется

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/agent/calendar/events
Success status200
Idempotency headerнет (безопасное чтение)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdтокен (sub)GuidДаRequired; не принимается из queryrecord-level фильтр OwnerUserId
TenantIdтокен (tenant_id)GuidДаRequired; не принимается из queryTenant isolation
FromqueryDateTimeOffsetДаUTC; начало диапазона выборки
ToqueryDateTimeOffsetДаUTC; To > From; To − From ≤ 92 дня (квартальное окно)
KindqueryEventKind?Нетenum-строка Showing/Consultation/Meeting/Task/BookingDeadline (calendar-event.md)
StatusqueryEventStatus?Нетenum-строка Planned/InProgress/Completed/Cancelled
DealIdqueryGuid?Нетсобытия, привязанные к сделке CRM (в т.ч. дедлайны брони)
LeadIdqueryGuid?Нетсобытия, привязанные к лиду CRM
AppointmentIdqueryGuid?Нетсобытие конкретной записи (связь 1:1 — не более одного результата)
Page / PageSizequeryintНетPage ≥ 1; PageSize 1..500, default 200 (месяц активного агента помещается в одну страницу)

Все фильтры комбинируются по «И». Без фильтра Status возвращаются события во всех статусах, включая терминальные (Completed/Cancelled) — скрытие отменённых — забота клиента.

Модель ответа — страница CalendarEventModel

Ответ — страница с полями Items, Page, PageSize, TotalCount. Сортировка — StartAt по возрастанию, затем Id (стабильный порядок). Дом модели — этот документ; поля — проекция агрегата calendar-event.md поле-в-поле плюс вычисляемый HasConflicts.

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаentityИдентификатор события.
OwnerUserIdGuidДаentityВладелец календаря (в agent-области всегда равен актору; поле нужно для переиспользования модели в team-/pin-manager-выборках).
KindEventKindДаentityВид события.
OriginEventOriginДаentityПроисхождение (Manual/Appointment/CrmBooking) — определяет, какие операции доступны (INV-CAL-3/4).
StatusEventStatusДаentityТекущий статус.
PriorityEventPriorityДаentityПриоритет.
StartAtDateTimeOffsetДаentityНачало, UTC.
EndAtDateTimeOffset?НетentityКонец, UTC; null — точечное событие.
TimeZonestringДаentityIANA-таймзона события.
Titlestring?Нетentityв логи не пишетсяЗаголовок; при null клиент отображает производный заголовок по Kind.
Descriptionstring?Нетentityв логи не пишетсяОписание/заметки владельца.
Locationstring?Нетentityв логи не пишетсяМесто встречи (свободный текст).
MeetingLinkstring?НетentityСсылка на онлайн-встречу (Consultation/Meeting).
ClientUserIdGuid?НетentityКлиент-инвестор; display-данные — из реплики identity на клиенте.
AppointmentIdGuid?НетentityПорождающая запись (Origin = Appointment).
DealIdGuid?НетentityСделка CRM (логическая ссылка).
LeadIdGuid?НетentityЛид CRM (логическая ссылка).
BookingIdGuid?НетentityБронь CRM (Origin = CrmBooking).
PropertyObjectIdGuid?НетentityОбъект недвижимости (chessboard).
ProjectIdGuid?НетentityПроект застройщика (chessboard).
ReminderOffsetsMinutesint[]ДаentityОфсеты напоминаний «за N минут до начала»; пустой массив — без напоминаний.
CancelReasonstring?НетentityПричина отмены (заполнена при Status = Cancelled).
CreatedAtDateTimeOffsetДаentityСоздание.
UpdatedAtDateTimeOffsetДаentityПоследнее изменение.
VersionintДаentityВерсия строки — передаётся обратно в update-event.md / reschedule-event.md (оптимистическая блокировка).
HasConflictsboolДаcomputedПризнак пересечения с другим активным событием владельца (правило ниже, INV-CAL-6).

Правило выборки диапазона

Событие попадает в выборку, если его интервал пересекается с диапазоном запроса:

  • интервальное событие (EndAt задан): StartAt < To и EndAt > From;
  • точечное событие (EndAt = null): From ≤ StartAt < To.

Событие, начавшееся до From и закончившееся после (длинная встреча на границе окна), попадает в выборку — клиент отрисовывает видимую часть.

Правило HasConflicts (INV-CAL-6)

  • HasConflicts = true у интервального события, если существует другое интервальное событие того же OwnerUserId в статусе Planned/InProgress, интервалы которых строго пересекаются: StartAt < other.EndAt и other.StartAt < EndAt. Соприкосновение встык (EndAt = other.StartAt) — не конфликт.
  • Точечные события (EndAt = null) в расчёте не участвуют — ни как источник, ни как контрагент.
  • Контрагент учитывается независимо от диапазона From/To и статуса самого события в выборке: пересечение с активным событием за границей окна тоже подсвечивается.
  • Конфликт — предупреждение, не ошибка: создание и перенос пересекающихся событий не блокируются (create-event.md, reschedule-event.md).

Алгоритм

  1. Контекст и доступ. Актор — владелец календаря; tenant — его компания; требуется calendar.events.view. OwnerUserId и TenantId берутся из токена, не из запроса.
  2. Проверка входа. From/To заданы, To > From, диапазон ≤ 92 дней; enum-значения Kind/Status (если заданы) допустимы; paging в границах (Page ≥ 1, PageSize 1..500) — иначе ValidationError.
  3. Что читаем. Страница событий владельца по правилу выборки диапазона с наложением фильтров Kind/Status/DealId/LeadId/AppointmentId (все — по «И»), сортировка StartAt возр., затем Id. Ведущий индекс — ix_calendar_events_owner_start (OwnerUserId, StartAt).
  4. Вычисление HasConflicts. Для каждого интервального события страницы — проверка строгого пересечения с активными (Planned/InProgress) интервальными событиями владельца (правило выше).
  5. Результат. Страница CalendarEventModel (Items, Page, PageSize, TotalCount) → HTTP 200. Пустая выборка — 200 с Items = [], не 404. Данные отдаются из source of truth calendar — всегда актуальны на момент чтения.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400From/To отсутствуют; To ≤ From; диапазон > 92 дней; enum Kind/Status вне значений; paging вне границ«Некорректные параметры выборки»no retry
UNAUTHORIZED401нет/просрочен JWTno retry
FORBIDDEN403нет права calendar.events.viewno retry

Acceptance-критерии

  • Happy path. Дано: у агента события всех видов в диапазоне месяца. Когда: GET с From/To этого месяца. Тогда: 200, страница CalendarEventModel, сортировка по StartAt возр.; события вне диапазона отсутствуют.
  • Диапазон обязателен. Дано: запрос без From или To. Тогда: 400 ValidationError.
  • Слишком широкое окно. Дано: To − From = 200 дней. Тогда: 400 ValidationError.
  • Фильтры. Дано: Kind = Showing, Status = Planned. Тогда: только запланированные показы; TotalCount соответствует фильтру.
  • Фильтр по сделке. Дано: DealId сделки с активной бронью. Тогда: в выборке — и ручные события сделки, и автособытие BookingDeadline (Origin = CrmBooking).
  • HasConflicts. Дано: два пересекающихся Planned-события владельца. Тогда: оба возвращаются с HasConflicts = true; событие встык (EndAt первого = StartAt второго) — HasConflicts = false.
  • Точечные вне конфликтов. Дано: задача без EndAt внутри интервала встречи. Тогда: у обоих HasConflicts = false (точечные не участвуют).
  • Долгое событие на границе. Дано: событие StartAt < From < EndAt. Тогда: событие в выборке.
  • Record-level. Дано: в tenant есть события другого агента. Тогда: в выборку они не попадают; переданные в query OwnerUserId/TenantId игнорируются (ключи — из токена).
  • Пустая выборка. Дано: в диапазоне событий нет. Тогда: 200, Items = [], TotalCount = 0.

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

  • Новые поля CalendarEventModel — additive nullable (без breaking change); удаление/переименование/смена типа — breaking. Неизвестное enum-значение в query → ValidationError; новые значения EventKind/ EventStatus в ответе клиент обязан переживать (forward-compatible отображение «прочее»).
  • Logs: TenantId, OwnerUserId, диапазон, применённые фильтры, TotalCount (без Title/Description/ Location); metrics: calendar_api_request_duration_seconds{route}; traces — сквозной traceparent.

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

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

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