POST /api/v1/agent/calendar/slots — Публикация слотов доступности (batch)
Назначение
Агент (или инвест-консультант ПИН) публикует свою доступность для самозаписи клиентов: задаёт локальные
окна («Вт, Чт 10:00–13:00»), дни недели, длительность слота и горизонт — сервис нарезает окна на конкретные
слоты (availability-slot.md) и выставляет их на витрину
(../public/list-available-slots.md). Ответ — отчёт публикации: сколько слотов создано и какие кандидаты
пропущены с причинами. Поштучного создания слотов нет — только batch.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | calendar |
| API area | agent |
| Feature group | slots |
| Статус | draft |
| Документ | docs/06-services/calendar/api/agent/publish-slots.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Агент или консультант ПИН — владелец публикуемых слотов) |
| Auth policy / Permissions | calendar.slots.manage |
| Scope (RBAC) | организация — TenantId = компания актора (агентство / ПИН) |
| Record-level | слоты создаются только на себя: AgentUserId = actor (публикация за другого агента не поддерживается) |
| Tenant isolation | TenantId в каждом запросе (ADR-0052) |
| Audit | create; BatchId — след публикации |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/agent/calendar/slots |
| Success status | 201 (батч создан; в отчёте возможно CreatedCount = 0) |
| Idempotency header | не применяется: повтор батча с теми же окнами структурно безопасен — все кандидаты пересекаются с уже созданными слотами и пропускаются (INV-SLT-2), отчёт «создано 0» |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required (компания актора) | Tenant isolation |
Kind | body | EventKind | Да | только Showing / Consultation (подмножество EventKind); иное значение → VALIDATION_FAILED | — |
DateFrom | body | DateOnly | Да | локальная дата начала горизонта (в TimeZone) | — |
DateTo | body | DateOnly | Да | >= DateFrom; горизонт целиком в прошлом → CAL_SLOT_BATCH_INVALID_WINDOW | — |
DaysOfWeek | body | DayOfWeek[] | Да | 1..7 значений Monday..Sunday, без дублей; пустой массив → CAL_SLOT_BATCH_INVALID_WINDOW | — |
Windows | body | SlotWindowModel[] | Да | 1..8 окон; в каждом StartLocalTime < EndLocalTime, иначе → CAL_SLOT_BATCH_INVALID_WINDOW | — |
SlotDurationMinutes | body | int | Да | 1..1440; <= 0 → CAL_SLOT_BATCH_INVALID_WINDOW | — |
TimeZone | body | string | Да | валидный IANA-идентификатор, max 64; неизвестный → CAL_SLOT_TIMEZONE_UNKNOWN | — |
AutoConfirm | body | bool | Нет | default false — запись на слот требует решения агента | — |
CancellationCutoffMinutes | body | int | Нет | >= 0, default 120 (INV-APT-3) | — |
MinLeadTimeMinutes | body | int | Нет | >= 0, default 60 (INV-SLT-5) | — |
ProjectId | body | Guid? | Нет | логическая ссылка на проект (chessboard), синхронно не проверяется | — |
PropertyObjectId | body | Guid? | Нет | логическая ссылка на объект недвижимости (chessboard), синхронно не проверяется | — |
Location | body | string? | Нет | 0..512, свободный текст (ADR-0064 п.7) | может содержать адрес — в логи не пишется |
Модель SlotWindowModel
| Поле | Тип | Обязательность | Валидация | Описание |
|---|---|---|---|---|
StartLocalTime | TimeOnly | Да | формат HH:mm | Начало окна доступности в локальном времени TimeZone. |
EndLocalTime | TimeOnly | Да | формат HH:mm, > StartLocalTime | Конец окна. |
Модель ответа PublishSlotsResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
BatchId | Guid | Да | генерируется публикацией | — | Идентификатор батча — ключ отчётов и массовой архивации (archive-slot.md). |
CreatedCount | int | Да | computed | — | Сколько слотов создано (Available). |
SkippedCount | int | Да | computed | — | Сколько кандидатов пропущено. |
Skipped | SkippedSlotCandidateModel[] | Да | computed | — | Пропущенные кандидаты с причинами; пустой массив, если пропусков нет. |
Модель SkippedSlotCandidateModel
| Поле | Тип | Обязательность | Описание |
|---|---|---|---|
StartAt | DateTimeOffset | Да | Начало пропущенного кандидата, UTC. |
EndAt | DateTimeOffset | Да | Конец кандидата, UTC. |
Reason | SlotSkipReason | Да | Причина пропуска, см. enum. |
Enum SlotSkipReason
| Значение | Описание |
|---|---|
Overlap = 1 | Кандидат пересекается с существующим слотом агента (Available/Booked) или с другим кандидатом батча (INV-SLT-2). |
LeadTime = 2 | StartAt кандидата раньше now + MinLeadTimeMinutes (INV-SLT-5). |
NonExistentLocalTime = 3 | Локальное время кандидата не существует в день перевода часов (DST). |
Алгоритм
Правила генерации — дом availability-slot.md, раздел «Правила batch-генерации»; здесь — порядок применения.
- Контекст и доступ. Актор — владелец календаря с permission
calendar.slots.manage(иначеCAL_SLOT_ACCESS_DENIED); слоты создаются наAgentUserId = actorв егоTenantId. - Проверка входа.
TimeZone— валидный IANA-идентификатор (иначеCAL_SLOT_TIMEZONE_UNKNOWN);Kind ∈ {Showing, Consultation}, длинаLocation ≤ 512(иначеVALIDATION_FAILED); окна, дни недели, длительность и горизонт корректны — начало окна < конца, длительность > 0, дни недели непусты, горизонт не целиком в прошлом (иначеCAL_SLOT_BATCH_INVALID_WINDOW). - Нарезка кандидатов. Кандидаты = локальные окна × подходящие дни недели горизонта × длительность:
каждое окно нарезается подряд идущими интервалами
SlotDurationMinutes; неполный хвост окна отбрасывается. Вычисление — в локальном времениTimeZone, хранение — UTC (DST-корректность: несуществующее локальное время → пропускNonExistentLocalTime, неоднозначное — первое вхождение). - Лимит. Если кандидатов больше 500 →
CAL_SLOT_BATCH_LIMIT_EXCEEDED, батч целиком отклоняется, ничего не создаётся. - Фильтрация. Кандидаты с
StartAt < now + MinLeadTimeMinutesпропускаются (LeadTime); кандидаты, пересекающиеся с существующими слотами агента вAvailable/Bookedили друг с другом, пропускаются (Overlap, INV-SLT-2). - Создание. Оставшиеся кандидаты создаются слотами
Availableс общимBatchIdи параметрами батча (Kind,AutoConfirm,CancellationCutoffMinutes,MinLeadTimeMinutes,TimeZone,ProjectId/PropertyObjectId/Location). Публикация атомарна: либо создан весь отчётный набор, либо ничего. - Результат.
{ BatchId, CreatedCount, SkippedCount, Skipped[] }→ HTTP201. Созданные слоты сразу видны на витрине свободных слотов.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | Kind вне {Showing, Consultation}, Location длиннее 512, некорректный формат полей | «Проверьте корректность полей запроса» | no retry |
CAL_SLOT_BATCH_INVALID_WINDOW | 400 | начало окна ≥ конца, длительность ≤ 0, пустые дни недели, DateTo < DateFrom, горизонт целиком в прошлом | «Некорректные параметры окон доступности» | no retry |
CAL_SLOT_TIMEZONE_UNKNOWN | 400 | TimeZone — не валидный IANA-идентификатор | «Неизвестная таймзона» | no retry |
CAL_SLOT_BATCH_LIMIT_EXCEEDED | 400 | батч породил бы более 500 слотов | «Слишком большой батч: уменьшите горизонт или окна» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
CAL_SLOT_ACCESS_DENIED | 403 | нет permission calendar.slots.manage | «Недостаточно прав» | no retry |
Совместимость и наблюдаемость
- Новые optional параметры батча — additive; изменение семантики генерации (лимит, правила пропуска),
удаление/переименование полей — breaking. Unknown enum во входе (
Kind,DaysOfWeek) —VALIDATION_FAILED, не игнор. - Logs:
slots published(TenantId,AgentUserId,BatchId,CreatedCount,SkippedCount;Locationне пишется); metrics:calendar_slots_published_total{result},calendar_slots_skipped_total{reason}, latency p95 < 1 c; traces — сквозной trace черезtraceparent.
Постусловия
После 201: в TenantId актора существует батч слотов Available с общим BatchId; каждый созданный слот
удовлетворяет INV-SLT-1/2; слоты немедленно видны анонимной витрине (../public/list-available-slots.md) и
в списке агента (list-slots.md). Интеграционные события не публикуются — публикация слотов
внутреннее дело контекста (availability-slot.md).
Acceptance Criteria
- AC-1 (генерация). Given окно «Вт, Чт 10:00–13:00 Europe/Moscow», длительность 60 мин, горизонт
2 недели. When публикация. Then
201: создано 12 слотовAvailable(по 3 на каждый из 4 дней), время в UTC,SkippedCount = 0. - AC-2 (DST). Given окно 10:00–12:00 в таймзоне с переводом часов, горизонт охватывает дату перевода.
When публикация. Then слоты по обе стороны перевода начинаются в 10:00 локальных (разные UTC-моменты);
несуществующее локальное время — пропуск
NonExistentLocalTime. - AC-3 (пересечение). Given у агента уже есть
Available-слот 10:00–11:00 на вторник. When батч с окном 10:00–12:00 на вторник. Then кандидат 10:00–11:00 вSkipped(Overlap), кандидат 11:00–12:00 создан. - AC-4 (лимит). Given параметры порождают 600 кандидатов. When публикация. Then
400 CAL_SLOT_BATCH_LIMIT_EXCEEDED, ни один слот не создан. - AC-5 (повтор батча). Given батч уже опубликован. When повторная публикация тех же окон. Then
201сCreatedCount = 0— все кандидаты пропущены какOverlap, дублей нет. - AC-6 (таймзона). Given
TimeZone = "Europe/Nowhere". When публикация. Then400 CAL_SLOT_TIMEZONE_UNKNOWN. - AC-7 (нет прав). Given актор без
calendar.slots.manage. When публикация. Then403 CAL_SLOT_ACCESS_DENIED.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): GET /api/public/calendar/slots — Витрина свободных слотов самозаписи, GET /api/v1/agent/calendar/slots — Список слотов доступности агента, GET /api/v1/pin-manager/calendar/appointments — Записи к консультантам ПИН (read-only), POST /api/v1/agent/calendar/slots/archive — Архивация слотов (поштучно или по батчу)
- Use Cases (1): UC-CAL-002. Агент публикует слоты доступности
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: availability-slot.md (дом полей, правила генерации,
INV-SLT-1..5,
SlotStatus); порождаемые записи: appointment.md. - Соседние операции: list-slots.md, archive-slot.md; витрина и
бронирование:
../public/list-available-slots.md,../public/book-slot.md; истечение слотов:../../background/slot-expiration.md. - ADR: 0064 calendar п.3/6 (материализованные слоты, DST, лимит батча), 0056 simplicity-first.