GET /api/v1/agent/calendar/events — Список событий календаря агента
Назначение
Отдаёт владельцу календаря (агенту агентства или консультанту ПИН) его события за обязательный диапазон
дат: показы, консультации, встречи, задачи и автоматические дедлайны брони — с признаком пересечения
HasConflicts по каждому событию. Это основная выборка календарной сетки (день/неделя/месяц) в кабинете
агента. Документ — дом модели CalendarEventModel, правила выборки диапазона и правила вычисления
HasConflicts: остальные выборки событий контекста ссылаются сюда.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | calendar |
| API area | agent |
| Feature group | calendar-events |
| Статус | draft |
| Документ | docs/06-services/calendar/api/agent/list-events.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (агент агентства / консультант ПИН — владелец календаря) |
| Auth policy / Permissions | calendar.events.view |
| Scope (RBAC) | организация: компания актора (агентство / ПИН) |
| Record-level | OwnerUserId = ActorUserId из токена (sub), не из query — актор видит только собственные события; чужие события в выборку не попадают (fail-closed). Календарь команды — отдельная операция ../agency/list-team-events.md (calendar.team.view) |
| Tenant isolation | TenantId из токена (tenant_id); изоляция применяется независимо от параметров запроса |
| Audit | read: актор и фильтры фиксируются в trace; содержимое событий (Title/Description/Location — могут содержать ПДн клиента) в логи не пишется |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/v1/agent/calendar/events |
| Success status | 200 |
| Idempotency header | нет (безопасное чтение) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | токен (sub) | Guid | Да | Required; не принимается из query | record-level фильтр OwnerUserId |
TenantId | токен (tenant_id) | Guid | Да | Required; не принимается из query | Tenant isolation |
From | query | DateTimeOffset | Да | UTC; начало диапазона выборки | — |
To | query | DateTimeOffset | Да | UTC; To > From; To − From ≤ 92 дня (квартальное окно) | — |
Kind | query | EventKind? | Нет | enum-строка Showing/Consultation/Meeting/Task/BookingDeadline (calendar-event.md) | — |
Status | query | EventStatus? | Нет | enum-строка Planned/InProgress/Completed/Cancelled | — |
DealId | query | Guid? | Нет | события, привязанные к сделке CRM (в т.ч. дедлайны брони) | — |
LeadId | query | Guid? | Нет | события, привязанные к лиду CRM | — |
AppointmentId | query | Guid? | Нет | событие конкретной записи (связь 1:1 — не более одного результата) | — |
Page / PageSize | query | int | Нет | Page ≥ 1; PageSize 1..500, default 200 (месяц активного агента помещается в одну страницу) | — |
Все фильтры комбинируются по «И». Без фильтра Status возвращаются события во всех статусах,
включая терминальные (Completed/Cancelled) — скрытие отменённых — забота клиента.
Модель ответа — страница CalendarEventModel
Ответ — страница с полями Items, Page, PageSize, TotalCount. Сортировка — StartAt по
возрастанию, затем Id (стабильный порядок). Дом модели — этот документ; поля — проекция агрегата
calendar-event.md поле-в-поле плюс вычисляемый HasConflicts.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | entity | — | Идентификатор события. |
OwnerUserId | Guid | Да | entity | — | Владелец календаря (в agent-области всегда равен актору; поле нужно для переиспользования модели в team-/pin-manager-выборках). |
Kind | EventKind | Да | entity | — | Вид события. |
Origin | EventOrigin | Да | entity | — | Происхождение (Manual/Appointment/CrmBooking) — определяет, какие операции доступны (INV-CAL-3/4). |
Status | EventStatus | Да | entity | — | Текущий статус. |
Priority | EventPriority | Да | entity | — | Приоритет. |
StartAt | DateTimeOffset | Да | entity | — | Начало, UTC. |
EndAt | DateTimeOffset? | Нет | entity | — | Конец, UTC; null — точечное событие. |
TimeZone | string | Да | entity | — | IANA-таймзона события. |
Title | string? | Нет | entity | в логи не пишется | Заголовок; при null клиент отображает производный заголовок по Kind. |
Description | string? | Нет | entity | в логи не пишется | Описание/заметки владельца. |
Location | string? | Нет | entity | в логи не пишется | Место встречи (свободный текст). |
MeetingLink | string? | Нет | entity | — | Ссылка на онлайн-встречу (Consultation/Meeting). |
ClientUserId | Guid? | Нет | entity | — | Клиент-инвестор; display-данные — из реплики identity на клиенте. |
AppointmentId | Guid? | Нет | entity | — | Порождающая запись (Origin = Appointment). |
DealId | Guid? | Нет | entity | — | Сделка CRM (логическая ссылка). |
LeadId | Guid? | Нет | entity | — | Лид CRM (логическая ссылка). |
BookingId | Guid? | Нет | entity | — | Бронь CRM (Origin = CrmBooking). |
PropertyObjectId | Guid? | Нет | entity | — | Объект недвижимости (chessboard). |
ProjectId | Guid? | Нет | entity | — | Проект застройщика (chessboard). |
ReminderOffsetsMinutes | int[] | Да | entity | — | Офсеты напоминаний «за N минут до начала»; пустой массив — без напоминаний. |
CancelReason | string? | Нет | entity | — | Причина отмены (заполнена при Status = Cancelled). |
CreatedAt | DateTimeOffset | Да | entity | — | Создание. |
UpdatedAt | DateTimeOffset | Да | entity | — | Последнее изменение. |
Version | int | Да | entity | — | Версия строки — передаётся обратно в update-event.md / reschedule-event.md (оптимистическая блокировка). |
HasConflicts | bool | Да | 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).
Алгоритм
- Контекст и доступ. Актор — владелец календаря; tenant — его компания; требуется
calendar.events.view.OwnerUserIdиTenantIdберутся из токена, не из запроса. - Проверка входа.
From/Toзаданы,To > From, диапазон ≤ 92 дней; enum-значенияKind/Status(если заданы) допустимы; paging в границах (Page ≥ 1,PageSize 1..500) — иначеValidationError. - Что читаем. Страница событий владельца по правилу выборки диапазона с наложением фильтров
Kind/Status/DealId/LeadId/AppointmentId(все — по «И»), сортировкаStartAtвозр., затемId. Ведущий индекс —ix_calendar_events_owner_start(OwnerUserId,StartAt). - Вычисление
HasConflicts. Для каждого интервального события страницы — проверка строгого пересечения с активными (Planned/InProgress) интервальными событиями владельца (правило выше). - Результат. Страница
CalendarEventModel(Items,Page,PageSize,TotalCount) → HTTP200. Пустая выборка —200сItems = [], не404. Данные отдаются из source of truthcalendar— всегда актуальны на момент чтения.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | From/To отсутствуют; To ≤ From; диапазон > 92 дней; enum Kind/Status вне значений; paging вне границ | «Некорректные параметры выборки» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | — | no retry |
FORBIDDEN | 403 | нет права calendar.events.view | — | no 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.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (9): GET /api/v1/agency/calendar/events — Календарь команды (read-only), GET /api/v1/agent/calendar/events/{id} — Карточка события календаря, GET /api/v1/pin-manager/calendar/events — Календари консультантов ПИН (read-only), POST /api/v1/agent/calendar/events — Создание события календаря, POST /api/v1/agent/calendar/events/{id}/cancel — Отмена события (→ Cancelled), POST /api/v1/agent/calendar/events/{id}/complete — Завершение события (→ Completed), POST /api/v1/agent/calendar/events/{id}/reschedule — Перенос события календаря, POST /api/v1/agent/calendar/events/{id}/start — Начало события (Planned → InProgress), PUT /api/v1/agent/calendar/events/{id} — Изменение события календаря
- Use Cases (2): UC-CAL-007. Дедлайны брони CRM в календаре агента, UC-CAL-011. Переназначение открытых дедлайнов и встреч при смене исполнителя
- registries (1): Реестр API-методов PIN
Связанные документы
- Агрегат и правило конфликтов INV-CAL-6: calendar-event.md; офсеты напоминаний: event-reminder.md.
- Операции над событием: get-event.md, create-event.md, update-event.md, reschedule-event.md, start-event.md, complete-event.md, cancel-event.md.
- Календарь команды (руководитель):
../agency/list-team-events.md; календарь консультантов ПИН:../pin-manager/list-events.md. - ADR-0064 — границы контекста; ADR-0052 — tenancy.