GET /api/public/calendar/slots — Витрина свободных слотов самозаписи
Назначение
Публичная витрина доступности: аноним или инвестор видит свободные слоты агента (или инвест-консультанта
ПИН) для записи на показ или консультацию — по конкретному агенту, проекту застройщика или объекту
недвижимости. Отдаются только бронируемые слоты (Available и не позже lead-time) с display-именем агента
без контактов и прочих персональных данных. Выбранный слот бронируется операцией
book-slot.md.
| Поле | Значение |
|---|
| Сервис/контекст | calendar |
| API area | public |
| Feature group | slots |
| Статус | draft |
| Документ | docs/06-services/calendar/api/public/list-available-slots.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | Anonymous или User (Инвестор с JWT — контракт одинаков) |
| Auth policy / Permissions | none (публичный метод); rate limit по IP |
| Scope (RBAC) | не применяется (актор — вне tenant) |
| Tenant isolation | не применяется: витрина читает слоты всех компаний (Available — публичное предложение времени, availability-slot.md) |
| Audit | read: фильтры фиксируются в trace; PII в запросе и ответе нет |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/public/calendar/slots |
| Success status | 200 |
| Idempotency header | нет (безопасное чтение) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Хотя бы один из целевых фильтров AgentUserId / ProjectId / PropertyObjectId обязателен — витрина
не отдаёт слоты «всей платформы» без цели. Все фильтры комбинируются по «И».
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
AgentUserId | query | Guid? | Условно | слоты конкретного агента/консультанта | — |
ProjectId | query | Guid? | Условно | слоты показов по проекту застройщика (chessboard, логическая ссылка) | — |
PropertyObjectId | query | Guid? | Условно | слоты показов по конкретному объекту | — |
Kind | query | EventKind? | Нет | enum-строка Showing / Consultation (подмножество слотов, availability-slot.md); иное значение → VALIDATION_FAILED | — |
From | query | DateTimeOffset? | Нет | UTC; default now; значения раньше now поднимаются до now (прошедшие слоты не отдаются никогда) | — |
To | query | DateTimeOffset? | Нет | UTC; To > From; default From + 14 дней; To − From ≤ 31 день | — |
Page / PageSize | query | int | Нет | Page ≥ 1; PageSize 1..200, default 100 | — |
Модель ответа — страница PublicSlotModel
Ответ — страница с полями Items, Page, PageSize, TotalCount; сортировка — StartAt по
возрастанию, затем Id (стабильный порядок). Дом модели — этот документ; поля — публичная проекция
availability-slot.md без PII: данных клиентов в слоте нет, из
данных агента — только display-имя.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
SlotId | Guid | Да | entity | — | Идентификатор слота — вход операции book-slot.md. |
AgentUserId | Guid | Да | entity | — | Агент/консультант — владелец слота. |
AgentDisplayName | string? | Нет | display-реплика identity (UserDisplayReplicaEntity) | без контактов | Отображаемое имя агента («Имя Фамилия»); null, если реплика ещё не получила пользователя (клиент показывает обезличенный вариант). Телефон/email/прочие данные агента не отдаются. |
Kind | EventKind | Да | entity | — | Вид встречи: Showing / Consultation. |
StartAt | DateTimeOffset | Да | entity | — | Начало слота, UTC. |
EndAt | DateTimeOffset | Да | entity | — | Конец слота, UTC. |
TimeZone | string | Да | entity | — | IANA-таймзона слота — локальное отображение времени на клиенте. |
AutoConfirm | bool | Да | entity | — | true — запись подтверждается мгновенно; false — потребуется решение агента. |
CancellationCutoffMinutes | int | Да | entity | — | Правило отмены: инвестор сможет отменить/перенести запись не позже чем за столько минут до начала (INV-APT-3). |
BookableUntil | DateTimeOffset | Да | computed: StartAt − MinLeadTimeMinutes | — | До какого момента слот можно забронировать (INV-SLT-5). |
ProjectId | Guid? | Нет | entity | — | Проект застройщика (логическая ссылка). |
PropertyObjectId | Guid? | Нет | entity | — | Объект недвижимости (логическая ссылка). |
Location | string? | Нет | entity | — | Место встречи — свободный текст, заданный агентом для витрины. |
Алгоритм
- Контекст и доступ. Публичный метод без аутентификации; rate limit по IP — превышение →
CAL_RATE_LIMITED.
- Проверка входа. Задан хотя бы один из
AgentUserId/ProjectId/PropertyObjectId; Kind (если
задан) ∈ {Showing, Consultation}; To > From, окно ≤ 31 дня; paging в границах — иначе
VALIDATION_FAILED. Существование агента/проекта/объекта синхронно не проверяется: неизвестный id даёт
пустую выборку (fail-closed, без раскрытия существования).
- Что читаем. Страница слотов:
Status = Available, StartAt в окне [From, To) и слот ещё
бронируем — now ≤ StartAt − MinLeadTimeMinutes (per-slot значение, INV-SLT-5); наложение фильтров
AgentUserId/ProjectId/PropertyObjectId/Kind по «И»; сортировка StartAt возр., затем Id.
Ведущий индекс — ix_availability_slots_status_start (Status, StartAt). Display-имена агентов
страницы дополняются из display-реплики identity одним заходом.
- Результат. Страница
PublicSlotModel (Items, Page, PageSize, TotalCount) → HTTP 200;
пустая выборка — 200 с Items = [], не 404. Freshness: витрина не резервирует слот — между
просмотром и бронированием слот может занять другой клиент (hold'ов нет, ADR-0064 п.3); актуальная
проверка выполняется в book-slot.md, проигравший получает SLOT_ALREADY_BOOKED.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | нет ни одного целевого фильтра; To ≤ From; окно > 31 дня; enum Kind вне значений; paging вне границ | «Некорректные параметры выборки» | no retry |
CAL_RATE_LIMITED | 429 | превышен rate limit по IP | «Слишком много запросов, повторите позже» | retry после окна лимита |
Совместимость и наблюдаемость
- Новые поля
PublicSlotModel — additive optional (без breaking change); удаление/переименование/смена
типа — breaking. Неизвестное enum-значение Kind в query → VALIDATION_FAILED; новые значения
EventKind в ответе публичный клиент обязан переживать (forward-compatible отображение «встреча»).
- Logs: применённые фильтры, окно,
TotalCount (PII в логах нет — её нет и в данных); metrics:
calendar_api_request_duration_seconds{route}, calendar_public_slots_requests_total{result}; traces —
сквозной traceparent. Latency p95 < 300 мс.
Acceptance Criteria
- AC-1 (по агенту). Дано: у агента 5 слотов
Available в ближайшие 2 недели. Когда: GET с
AgentUserId без From/To. Тогда: 200, все 5 слотов, сортировка StartAt возр., у каждого —
AgentDisplayName без контактов.
- AC-2 (по проекту). Дано: слоты показов трёх агентов с одним
ProjectId. Когда: GET с ProjectId
и Kind = Showing. Тогда: слоты всех трёх агентов; консультационные слоты не возвращаются.
- AC-3 (занятый скрыт). Дано: слот перешёл в
Booked. Когда: повторный запрос витрины. Тогда: слот в
выборке отсутствует.
- AC-4 (lead time). Дано: слот с
StartAt через 30 минут и MinLeadTimeMinutes = 60. Тогда: слот в
выборке отсутствует (уже не бронируем), хотя статус ещё Available.
- AC-5 (без цели). Дано: запрос без
AgentUserId/ProjectId/PropertyObjectId. Тогда: отказ
VALIDATION_FAILED (400).
- AC-6 (широкое окно). Дано:
To − From = 60 дней. Тогда: отказ VALIDATION_FAILED (400).
- AC-7 (неизвестный агент). Дано:
AgentUserId, которого нет. Тогда: 200, Items = [] (без
раскрытия существования).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы