UC-CAL-003. Инвестор бронирует слот с витрины
Назначение
Описывает самозапись: инвестор (или аноним, регистрируемый по телефону на лету) выбирает на витрине
свободный слот агента и бронирует его. Бронирование атомарно связывает три сущности — слот переходит
Available → Booked, создаётся запись (Pending или сразу Confirmed по AutoConfirm слота) и
календарное событие агента (Planned, Origin = Appointment). CRM узнаёт о записи только по событию
appointment-created (get-or-create клиента/лида + лента лида), уведомления доставляет notifications.
Связан с контекстом calendar.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CAL-003 |
| Контекст | calendar (см. docs/06-services/calendar/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор (авторизованный) или Гость (аноним — lazy-регистрация по телефону, как в create-consultation-request) |
| Permissions / scope | Просмотр витрины и бронирование — публичные (rate limit по IP+телефону); permission-модель tenant-а не применяется |
| Tenant | null — инвестор вне tenant (ADR-0052); запись создаётся в tenant компании агента |
| Record-level | Созданная запись доступна инвестору по InvestorUserId (../api/investor/list-my-appointments.md) |
Предусловие
- Агент опубликовал слоты (UC-CAL-002); интересующий слот
Availableи до его начала большеMinLeadTimeMinutes. - У анонима есть телефон для lazy-регистрации; авторизованный инвестор действует под своим JWT.
- Сервис
identityдоступен (для lazy-регистрации анонима).
Триггер
| Поле | Значение |
|---|---|
| Источник | UI button |
| Триггерящий элемент | Витрина (страница агента / проекта / объекта) → «Записаться на показ/консультацию» |
Основной Поток
- Посетитель открывает витрину свободных слотов (
../api/public/list-available-slots.md, аноним): фильтры — агент, проект, объект недвижимости, вид встречи (Kind), диапазон дат. Система возвращаетAvailable-слоты с временем (StartAt/EndAt/TimeZone), видом, привязками (ProjectId/PropertyObjectId/Location) и display-данными агента (ФИО из реплики identity); PII клиентов витрина не отдаёт. - Посетитель выбирает слот и заполняет форму записи: телефон и ФИО (аноним) либо ничего дополнительно
(авторизованный инвестор), опционально
Comment(«интересует 2-комнатная…», ≤ 1024); клиент вызывает../api/public/book-slot.md. - Система проверяет вход (формат телефона E.164, длина
Comment) и rate limit по IP+телефону — превышение →CAL_RATE_LIMITED. - Для анонима система выполняет lazy-регистрацию через
identity(get-or-create пользователя-инвестора по телефону); недоступность identity →IDENTITY_UNAVAILABLE(503, A5). Авторизованный инвестор идентифицируется JWT. - Система проверяет доступность слота: статус
Available(иначеCAL_SLOT_NOT_AVAILABLE; проигранная гонка —SLOT_ALREADY_BOOKED, A1), lead timenow ≤ StartAt − MinLeadTimeMinutes(иначеCAL_SLOT_LEAD_TIME_VIOLATION, A3) и антиспам-лимиты инвестора (INV-APT-2: не более 1 активной записи на пару «инвестор + агент + вид», не более 3 активных записей суммарно — иначеAPPOINTMENT_DUPLICATE, A2). - В одном согласованном изменении система: переводит слот
Available → Booked(условный переход — выигрывает ровно один запрос, INV-SLT-3); создаёт запись со снапшотом времени слота (StartAt/EndAt/TimeZone/Kindиммутабельны, INV-APT-5) — статусConfirmedприAutoConfirm = true(сConfirmedAt) либоPendingс дедлайном решения агентаPendingExpiresAt = min(CreatedAt + 24 ч, StartAt − MinLeadTimeMinutes); создаёт календарное событие агента (Planned,Origin = Appointment, время/вид/место из слота) и материализует напоминания по офсетам события (event-reminder.md); публикуетpin.calendar.appointment-created.v1(при auto-confirm — такжеpin.calendar.appointment-confirmed.v1). - Система возвращает результат: идентификатор записи, статус (
Pending/Confirmed), снапшот времени, display-данные агента. Инвестор видит подтверждение («ожидает решения агента» / «встреча подтверждена»). - Асинхронно (< 1 мин штатно):
crmпоappointment-createdделает get-or-create клиента/лида по телефону и пишет ленту лида, включаяCommentинвестора (../../crm/consumers/calendar-appointments.md);notificationsуведомляет агента о новой записи и инвестора о статусе (../../notifications/consumers/calendar-events.md). Запись в календаре агента видна сразу — лаг касается только CRM-проекции и уведомлений.
Альтернативные Потоки
A1. Гонка за слот
- Два посетителя одновременно бронируют один слот: условный переход
Available → Bookedвыигрывает ровно один запрос; проигравший получаетSLOT_ALREADY_BOOKED(409), состояние консистентно — вторая запись не создана даже частично. - Клиент проигравшего обновляет витрину и предлагает выбрать другой слот. Завершается или возвращается к шагу 1.
A2. Антиспам-лимиты
- У инвестора уже есть активная запись к этому агенту того же вида, либо активных записей уже 3 → отказ
APPOINTMENT_DUPLICATE(409). Клиент показывает существующие записи инвестора (../api/investor/list-my-appointments.md) и предлагает отменить/перенести лишнюю (UC-CAL-005).
A3. Слот стал недоступен
- Слот истёк или заархивирован между просмотром витрины и бронированием →
CAL_SLOT_NOT_AVAILABLE; до начала меньшеMinLeadTimeMinutes→CAL_SLOT_LEAD_TIME_VIOLATION. Клиент обновляет список слотов.
A4. Повтор запроса бронирования
- Повторный
book-slotтого же инвестора на тот же слот (double-click, retry) отклоняется структурно: на слот не более одной активной записи (INV-APT-1), на пару «инвестор + агент + вид» — не более одной (INV-APT-2). Дублей записи не возникает; клиент показывает уже созданную запись.
A5. Identity недоступен
- Lazy-регистрация анонима не выполнена →
IDENTITY_UNAVAILABLE(503); бронирование отклонено целиком, слот остаётсяAvailable. Витрина продолжает работать read-only; посетитель повторяет попытку позже.
A6. Дальше — no-slot путь
- Посетителю не подходит ни один слот («время обсудим») — он оставляет заявку на консультацию без слота:
create-consultation-request (мягкий
PreferredAt-хинт). Оба входа воронки сходятся в CRM в одном клиенте/лиде — get-or-create по телефону на стороне CRM-консюмера. Завершается вне календаря.
Постусловие
- Слот
Booked; записьPending(сPendingExpiresAt) илиConfirmed(сConfirmedAt) со снапшотом времени слота; календарное событие агентаPlanned(Origin = Appointment, 1:1 с записью); reminder-строки материализованы — всё в одном согласованном изменении. - Опубликовано
appointment-created(+appointment-confirmedпри auto-confirm); CRM создала/нашла клиента и лида, лента лида пополнена; уведомления доставлены. - Аноним получил учётную запись инвестора (lazy-регистрация) и видит запись в «Мои записи».
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
VALIDATION_FAILED | 400 | Формат телефона/ФИО/Comment | Подсветить поля формы |
CAL_SLOT_NOT_FOUND | 404 | Слот не существует | Обновить витрину |
SLOT_ALREADY_BOOKED | 409 | Гонка за слот проиграна (A1) | Предложить другой слот |
CAL_SLOT_NOT_AVAILABLE | 409 | Слот Expired/Archived (A3) | Обновить витрину |
CAL_SLOT_LEAD_TIME_VIOLATION | 409 | До начала меньше MinLeadTimeMinutes (A3) | Предложить более поздний слот |
APPOINTMENT_DUPLICATE | 409 | Антиспам INV-APT-2 (A2) | Показать активные записи инвестора |
CAL_RATE_LIMITED | 429 | Превышен rate limit по IP+телефону (шаг 3) | Повторить позже (retry с backoff) |
IDENTITY_UNAVAILABLE | 503 | Identity недоступен при lazy-регистрации (A5) | Повторить позже |
Полная таблица ошибок бронирования — ../api/public/book-slot.md.
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
AppointmentCreatedEvent.v1 | pin.calendar.appointment-created.v1 | Создание записи (шаг 6) | crm (get-or-create клиента/лида + лента), notifications (AppointmentCreated) |
AppointmentConfirmedEvent.v1 | pin.calendar.appointment-confirmed.v1 | Auto-confirm при бронировании (шаг 6) | crm (лента лида), notifications (AppointmentConfirmed) |
CalendarEventCreatedEvent.v1 | pin.calendar.event-created.v1 | Создание порождённого события агента (шаг 6) | notifications |
Ключ партиционирования — AppointmentId (порядок событий одной записи гарантирован). Payload — без PII;
исключение — Comment инвестора в appointment-created (класс Internal, маскирование в логах): он нужен
CRM для ленты лида.
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Бронирование Available-слота с AutoConfirm = true атомарно даёт: слот Booked, запись Confirmed, событие Planned (Origin = Appointment), reminder-строки, appointment-created + appointment-confirmed | интеграционный тест auto-confirm |
| AC2 | Бронирование слота с AutoConfirm = false даёт запись Pending с PendingExpiresAt = min(CreatedAt + 24 ч, StartAt − MinLeadTimeMinutes) | тест ручного подтверждения |
| AC3 | Два одновременных book-slot на один слот → ровно одна запись; второй запрос получает SLOT_ALREADY_BOOKED, частичных данных нет | тест гонки (A1) |
| AC4 | Вторая активная запись на пару «инвестор + агент + вид» и четвёртая активная запись инвестора → APPOINTMENT_DUPLICATE | негативные тесты антиспама (A2) |
| AC5 | Бронирование за 30 минут до начала при MinLeadTimeMinutes = 60 → CAL_SLOT_LEAD_TIME_VIOLATION | негативный тест lead time (A3) |
| AC6 | Аноним с новым телефоном получает учётную запись (lazy-регистрация) и созданная запись привязана к ней; повторное бронирование с тем же телефоном использует существующего пользователя | тест lazy-регистрации |
| AC7 | При недоступности identity бронирование анонима отклоняется IDENTITY_UNAVAILABLE, слот остаётся Available (A5) | тест отказа зависимости |
| AC8 | По appointment-created CRM делает get-or-create клиента/лида по телефону и пишет ленту с Comment; слотовая запись и no-slot заявка с одним телефоном сходятся в одном клиенте | интеграционный тест воронки (A6) |
| AC9 | Снапшот времени записи неизменяем: последующая архивация/изменение параметров батча слота не меняет StartAt/EndAt записи | тест иммутабельности снапшота |
Наблюдаемость
- Логи (без PII).
slot-booked(slotId,appointmentId,agentUserId,investorUserId,tenantId,kind,autoConfirm); телефон — маска+7***NNNN;Commentв логи не пишется. - Метрики.
calendar_bookings_total{result}(доляSLOT_ALREADY_BOOKED— интенсивность гонок);calendar_booking_rate_limited_total;calendar_lazy_registrations_total; лаг CRM/notifications консюмеров < 1 мин (alert > 5 мин).
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (7): UC-CAL-002. Агент публикует слоты доступности, UC-CAL-004. Агент подтверждает или отклоняет запись, UC-CAL-005. Отмена и перенос записи, UC-CAL-008. Напоминания о событиях календаря, UC-CAL-009. Руководитель смотрит календарь команды, UC-CAL-010. Руководитель ПИН смотрит календари консультантов, US-реестр контекста calendar (US-CAL)
- scenarios (3): UC-OVR-001. Запрос консультации у агента недвижимости, UC-OVR-002. Запрос консультации у инвест-консультанта ПИН (+2a), UC-OVR-011. Самозапись инвестора: от слота до исхода встречи и лида в воронке
- index.md (1): Calendar Service
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- Domain: availability-slot.md (гонка
Available → Booked, INV-SLT-3/5), appointment.md (снапшот, INV-APT-1..5,PendingExpiresAt), calendar-event.md (порождённое событие, INV-CAL-3), event-reminder.md. - API:
../api/public/list-available-slots.md,../api/public/book-slot.md,../api/investor/list-my-appointments.md,../api/investor/get-my-appointment.md. - События:
../events/appointment-created.md,../events/appointment-confirmed.md, event-created.md. - CRM-сторона: create-consultation-request (no-slot
путь, lazy-регистрация, rate limit), consumer записей —
../../crm/consumers/calendar-appointments.md. - Сценарии: UC-OVR-001, UC-OVR-002.
- Связанные UC: UC-CAL-002 (публикация слотов),
UC-CAL-004 (решение агента по
Pending), UC-CAL-005 (отмена/перенос).