UC-CAL-004. Агент подтверждает или отклоняет запись
Назначение
Описывает решение агента по Pending-записи: инвестор записался на слот без auto-confirm, и агент должен
до дедлайна PendingExpiresAt подтвердить встречу или отклонить её с причиной. Непринятое вовремя решение
закрывает запись автоматически (таймаут worker-ом) — слот освобождается для других инвесторов, а инвестор
получает уведомление. Связан с контекстом calendar.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CAL-004 |
| Контекст | calendar (см. docs/06-services/calendar/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Агент (или консультант ПИН) — владелец слота, на который создана запись; таймаут выполняет System (worker) |
| Permissions / scope | calendar.appointments.view (просмотр), calendar.appointments.decide (confirm/decline) — scope Organization |
| Tenant | TenantId = CompanyId компании агента, ADR-0052 |
| Record-level | Агент решает только по записям на свои слоты (AgentUserId = актор) |
Предусловие
- Существует запись
Pendingна слот агента сAutoConfirm = false(UC-CAL-003);PendingExpiresAtещё не наступил. - Агент уведомлён о новой записи (
notifications, kindAppointmentCreated) либо видит её в списке записей.
Триггер
| Поле | Значение |
|---|---|
| Источник | UI button (подтверждение/отклонение) / Schedule (таймаут) |
| Триггерящий элемент | ЛК агента → «Календарь» → «Записи» → карточка Pending-записи; worker ../background/appointment-timeout.md |
Основной Поток
- Агент открывает список записей (
../api/agent/list-appointments.md, фильтр по статусуPending) или карточку записи (../api/agent/get-appointment.md): время (снапшот слота), вид встречи, display-данные инвестора (из реплики identity),Commentинвестора,PendingExpiresAt. - Агент нажимает «Подтвердить»; клиент вызывает
../api/agent/confirm-appointment.md. - Система проверяет: актор — агент записи с
calendar.appointments.decide; запись в статусеPendingиPendingExpiresAtне наступил (иначеAPPOINTMENT_INVALID_STATE, A3). - Запись переходит
Pending → Confirmed(ConfirmedAt = now); слот остаётсяBooked, календарное событие остаётсяPlanned(каскад-таблица — appointment.md); публикуетсяpin.calendar.appointment-confirmed.v1(AutoConfirmed = false). - Асинхронно:
crmпишет в ленту лида «встреча подтверждена»;notificationsуведомляет инвестора (AppointmentConfirmed). Напоминания уйдут штатным механизмом (UC-CAL-008). - Агент видит запись
Confirmedв списке; дальше — встреча и фиксация исхода (UC-CAL-006).
Альтернативные Потоки
A1. Отклонение записи
- Агент нажимает «Отклонить» и указывает причину (
CancelReason, обязателен — иначеVALIDATION_FAILED); клиент вызывает../api/agent/decline-appointment.md. - Запись переходит
Pending → Declined; каскадом в том же согласованном изменении: слот освобождается (Booked → Available, еслиnow ≤ StartAt − MinLeadTimeMinutes, иначеBooked → Expired), календарное событие →Cancelled(CancelReasonиз записи),Pending-напоминания →Skipped; публикуетсяpin.calendar.appointment-cancelled.v1(Reason = DeclinedByAgent). - Инвестор получает уведомление и может выбрать другой слот (UC-CAL-003). Завершается.
A2. Таймаут решения (worker)
- Агент не принял решение до
PendingExpiresAt. Worker../background/appointment-timeout.md(polling, построчная блокировка) переводит записьPending → Expired; каскад тот же, что при отклонении: слот освобождён (Available/Expired), событиеCancelled, напоминанияSkipped; публикуетсяappointment-cancelled(Reason = Expired). - Инвестор и агент уведомляются; повторный проход worker-а над уже не-
Pendingзаписью — no-op (ровно одноappointment-cancelledна запись). Завершается.
A3. Решение после дедлайна или по неактуальной записи
- Агент подтверждает/отклоняет запись после
PendingExpiresAt, либо запись уже переведена worker-ом вExpiredили отменена инвестором → отказAPPOINTMENT_INVALID_STATE(409). Клиент перечитывает запись и показывает актуальный статус.
A4. Гонка «агент ↔ инвестор ↔ worker»
- Подтверждение агента конкурирует с отменой инвестора (UC-CAL-005)
и таймаутом worker-а: побеждает первая зафиксированная мутация (оптимистическая блокировка по версии
строки), проигравший получает
APPOINTMENT_INVALID_STATEи перечитывает состояние. Каскады выполняются ровно один раз — победителем.
A5. Запись на слот с auto-confirm
- Для слотов
AutoConfirm = trueсценарий не возникает: запись создаётся сразуConfirmed(PendingExpiresAt = null), решение агента не требуется,appointment-confirmedпубликуется сAutoConfirmed = trueуже при бронировании.
Постусловие
- Запись — в одном из статусов:
Confirmed(встреча состоится; слотBooked, событиеPlanned),DeclinedилиExpired(слот освобождён или истёк, событиеCancelled, напоминанияSkipped). - По каждому переходу опубликовано ровно одно интеграционное событие (
appointment-confirmed/appointment-cancelled); лента лида CRM и уведомления пополнены асинхронно.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
APPOINTMENT_NOT_FOUND | 404 | Запись не найдена или не на слот актора | Обновить список записей |
APPOINTMENT_ACCESS_DENIED | 403 | Нет calendar.appointments.decide | Скрыть действия решения |
VALIDATION_FAILED | 400 | Отклонение без CancelReason (A1) | Запросить причину |
APPOINTMENT_INVALID_STATE | 409 | Запись не Pending; решение после PendingExpiresAt; проигранная гонка (A3, A4) | Перечитать запись |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
AppointmentConfirmedEvent.v1 | pin.calendar.appointment-confirmed.v1 | Pending → Confirmed (шаг 4) | crm (лента лида), notifications (AppointmentConfirmed) |
AppointmentCancelledEvent.v1 | pin.calendar.appointment-cancelled.v1 | Pending → Declined (A1, Reason = DeclinedByAgent); Pending → Expired (A2, Reason = Expired) | crm (лента лида), notifications (AppointmentCancelled) |
CalendarEventStatusChangedEvent.v1 | pin.calendar.event-status-changed.v1 | Каскадная отмена календарного события (A1, A2) | notifications |
Ключ — AppointmentId (EventId для события календаря): порядок переходов одной записи гарантирован.
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Подтверждение Pending-записи до PendingExpiresAt даёт Confirmed + ConfirmedAt; слот остаётся Booked, событие — Planned; опубликовано appointment-confirmed (AutoConfirmed = false) | интеграционный тест подтверждения |
| AC2 | Отклонение с причиной атомарно: запись Declined, слот освобождён (Available при достаточном lead time, иначе Expired), событие Cancelled, напоминания Skipped, appointment-cancelled (DeclinedByAgent) | интеграционный тест каскада (A1) |
| AC3 | Отклонение без причины → VALIDATION_FAILED, состояние не изменено | негативный тест (A1) |
| AC4 | Worker переводит просроченную Pending-запись в Expired с тем же каскадом и Reason = Expired; повторный проход — no-op, событие не дублируется | тест таймаута и идемпотентности (A2) |
| AC5 | Подтверждение после PendingExpiresAt или по записи в терминальном статусе → APPOINTMENT_INVALID_STATE | негативный тест (A3) |
| AC6 | Конкурентные confirm (агент) и cancel (инвестор): побеждает ровно один, проигравший получает 409, каскад выполнен однократно | тест гонки (A4) |
| AC7 | Запись auto-confirm-слота не попадает в очередь решений (PendingExpiresAt = null), worker её не трогает | тест auto-confirm (A5) |
| AC8 | Агент видит и решает только записи на свои слоты; чужая запись → APPOINTMENT_NOT_FOUND | тест record-level |
Наблюдаемость
- Логи (без PII).
appointment-confirmed/appointment-declined/appointment-expired(appointmentId,slotId,tenantId,agentUserId,reason?);CommentиCancelReason— длина, не текст. - Метрики.
calendar_appointments_decided_total{outcome}(confirmed/declined/expired — доляexpiredсигналит о неотвечающих агентах);calendar_appointment_timeout_lag_seconds(отставание worker-а отPendingExpiresAt); alert на рост зависшихPending.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (8): UC-CAL-001. Агент ведёт рабочий календарь, UC-CAL-002. Агент публикует слоты доступности, UC-CAL-003. Инвестор бронирует слот с витрины, UC-CAL-005. Отмена и перенос записи, UC-CAL-006. Агент фиксирует исход встречи, 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: appointment.md (матрица переходов, каскад-таблица, INV-APT-1..6), availability-slot.md (освобождение слота), calendar-event.md (INV-CAL-3), event-reminder.md (Skipped при отмене).
- API:
../api/agent/list-appointments.md,../api/agent/get-appointment.md,../api/agent/confirm-appointment.md,../api/agent/decline-appointment.md. - Worker:
../background/appointment-timeout.md. - События:
../events/appointment-confirmed.md,../events/appointment-cancelled.md,../events/event-status-changed.md. - Связанные UC: UC-CAL-003 (появление
Pending-записи), UC-CAL-005 (отмена/перенос), UC-CAL-006 (исход подтверждённой встречи).