GET /api/v1/agency/calendar/events — Календарь команды (read-only)
Назначение
Отдаёт руководителю агентства события календарей всех сотрудников его компании за диапазон дат: загрузку
агентов, показы, консультации и дедлайны броней — чтобы планировать распределение клиентов и контролировать
работу команды. Выборка строго read-only: управлять чужим событием нельзя ни одной операцией — жизненным
циклом события владеет только его владелец (agent-область) и системные каскады.
| Поле | Значение |
|---|
| Сервис/контекст | calendar |
| API area | agency |
| Feature group | team-calendar |
| Статус | draft |
| Документ | docs/06-services/calendar/api/agency/list-team-events.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (руководитель агентства / уполномоченный сотрудник) |
| Auth policy / Permissions | calendar.team.view |
| Scope (RBAC) | организация: компания актора |
| Record-level | не применяется — область команды: видны события всех владельцев внутри tenant актора (это и есть смысл permission) |
| Tenant isolation | TenantId из токена (tenant_id), не из query: события чужих компаний недоступны независимо от параметров (ADR-0052) |
| Audit | read-sensitive: актор, фильтры и OwnerUserId фиксируются в trace (руководитель читает чужие календари); Title/Description/Location в логи не пишутся |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/agency/calendar/events |
| Success status | 200 |
| Idempotency header | нет (безопасное чтение) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Все фильтры комбинируются по «И».
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
TenantId | токен (tenant_id) | Guid | Да | Required; не принимается из query | Tenant isolation |
From | query | DateTimeOffset | Да | UTC; начало диапазона | — |
To | query | DateTimeOffset | Да | UTC; To > From; To − From ≤ 92 дня (квартальное окно, как в list-events) | — |
OwnerUserId | query | Guid? | Нет | календарь конкретного сотрудника; сотрудник другого tenant → пустая выборка (fail-closed, без раскрытия существования) | — |
Kind | query | EventKind? | Нет | enum-строка Showing/Consultation/Meeting/Task/BookingDeadline | — |
Status | query | EventStatus? | Нет | enum-строка Planned/InProgress/Completed/Cancelled | — |
Page / PageSize | query | int | Нет | Page ≥ 1; PageSize 1..500, default 200 | — |
Модель ответа — страница CalendarEventModel
Модель, правило выборки диапазона и правило HasConflicts — по дому
list-events.md: поля идентичны, OwnerUserId в каждой строке указывает
сотрудника-владельца. Сортировка — OwnerUserId, затем StartAt возр., затем Id (события группируются
по сотрудникам). HasConflicts вычисляется в пределах календаря каждого владельца отдельно (пересечения
событий разных сотрудников конфликтом не являются).
Алгоритм
- Контекст и доступ. Актор — сотрудник компании с
calendar.team.view; нет права → FORBIDDEN.
TenantId берётся из токена.
- Проверка входа.
From/To заданы, To > From, диапазон ≤ 92 дней; enum-значения допустимы;
paging в границах — иначе VALIDATION_FAILED.
- Что читаем. Страница событий
TenantId = tenant актора по правилу выборки диапазона
(list-events) с наложением фильтров OwnerUserId/Kind/Status по «И»;
сортировка OwnerUserId, StartAt возр., Id.
- Вычисление
HasConflicts. Per-owner: для каждого интервального события — строгое пересечение с
активными интервальными событиями того же владельца.
- Результат. Страница
CalendarEventModel → HTTP 200; пустая выборка — 200 с Items = [].
Данные — из source of truth calendar, актуальны на момент чтения.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | From/To отсутствуют; To ≤ From; диапазон > 92 дней; enum вне значений; paging вне границ | «Некорректные параметры выборки» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | — | no retry |
FORBIDDEN | 403 | нет права calendar.team.view | — | no retry |
Совместимость и наблюдаемость
- Контракт модели — по дому list-events.md (additive-правила там же); правило
сортировки agency-выборки (
OwnerUserId первым) — стабильная часть контракта.
- Logs:
TenantId, актор, OwnerUserId-фильтр, диапазон, TotalCount (без содержимого событий);
metrics: calendar_api_request_duration_seconds{route}; traces — сквозной traceparent.
Acceptance Criteria
- AC-1 (вся команда). Дано: в агентстве 3 агента с событиями в диапазоне. Когда:
GET с From/To
без OwnerUserId. Тогда: 200, события всех трёх агентов, сгруппированы по OwnerUserId.
- AC-2 (один сотрудник). Дано:
OwnerUserId агента из tenant актора. Тогда: только его события.
- AC-3 (tenant isolation). Дано:
OwnerUserId агента другой компании. Тогда: 200,
Items = [] — существование чужого календаря не раскрывается.
- AC-4 (нет права). Дано: агент без
calendar.team.view. Тогда: 403 FORBIDDEN (свои события он
смотрит через list-events).
- AC-5 (per-owner конфликты). Дано: события двух агентов пересекаются по времени между собой, но не
внутри своих календарей. Тогда: у обоих
HasConflicts = false.
- AC-6 (read-only). Дано: руководитель с
calendar.team.view и без calendar.events.manage. Тогда:
никакая операция изменения чужого события ему недоступна — в agency-области таких операций нет.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы