Перейти к основному содержимому

UC-CAL-004. Агент подтверждает или отклоняет запись

Назначение

Описывает решение агента по Pending-записи: инвестор записался на слот без auto-confirm, и агент должен до дедлайна PendingExpiresAt подтвердить встречу или отклонить её с причиной. Непринятое вовремя решение закрывает запись автоматически (таймаут worker-ом) — слот освобождается для других инвесторов, а инвестор получает уведомление. Связан с контекстом calendar.

Metadata

ПолеЗначение
IDUC-CAL-004
Контекстcalendar (см. docs/06-services/calendar/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАгент (или консультант ПИН) — владелец слота, на который создана запись; таймаут выполняет System (worker)
Permissions / scopecalendar.appointments.view (просмотр), calendar.appointments.decide (confirm/decline) — scope Organization
TenantTenantId = CompanyId компании агента, ADR-0052
Record-levelАгент решает только по записям на свои слоты (AgentUserId = актор)

Предусловие

  • Существует запись Pending на слот агента с AutoConfirm = false (UC-CAL-003); PendingExpiresAt ещё не наступил.
  • Агент уведомлён о новой записи (notifications, kind AppointmentCreated) либо видит её в списке записей.

Триггер

ПолеЗначение
ИсточникUI button (подтверждение/отклонение) / Schedule (таймаут)
Триггерящий элементЛК агента → «Календарь» → «Записи» → карточка Pending-записи; worker ../background/appointment-timeout.md

Основной Поток

  1. Агент открывает список записей (../api/agent/list-appointments.md, фильтр по статусу Pending) или карточку записи (../api/agent/get-appointment.md): время (снапшот слота), вид встречи, display-данные инвестора (из реплики identity), Comment инвестора, PendingExpiresAt.
  2. Агент нажимает «Подтвердить»; клиент вызывает ../api/agent/confirm-appointment.md.
  3. Система проверяет: актор — агент записи с calendar.appointments.decide; запись в статусе Pending и PendingExpiresAt не наступил (иначе APPOINTMENT_INVALID_STATE, A3).
  4. Запись переходит Pending → Confirmed (ConfirmedAt = now); слот остаётся Booked, календарное событие остаётся Planned (каскад-таблица — appointment.md); публикуется pin.calendar.appointment-confirmed.v1 (AutoConfirmed = false).
  5. Асинхронно: crm пишет в ленту лида «встреча подтверждена»; notifications уведомляет инвестора (AppointmentConfirmed). Напоминания уйдут штатным механизмом (UC-CAL-008).
  6. Агент видит запись Confirmed в списке; дальше — встреча и фиксация исхода (UC-CAL-006).

Альтернативные Потоки

A1. Отклонение записи

  1. Агент нажимает «Отклонить» и указывает причину (CancelReason, обязателен — иначе VALIDATION_FAILED); клиент вызывает ../api/agent/decline-appointment.md.
  2. Запись переходит Pending → Declined; каскадом в том же согласованном изменении: слот освобождается (Booked → Available, если now ≤ StartAt − MinLeadTimeMinutes, иначе Booked → Expired), календарное событие → Cancelled (CancelReason из записи), Pending-напоминания → Skipped; публикуется pin.calendar.appointment-cancelled.v1 (Reason = DeclinedByAgent).
  3. Инвестор получает уведомление и может выбрать другой слот (UC-CAL-003). Завершается.

A2. Таймаут решения (worker)

  1. Агент не принял решение до PendingExpiresAt. Worker ../background/appointment-timeout.md (polling, построчная блокировка) переводит запись Pending → Expired; каскад тот же, что при отклонении: слот освобождён (Available/Expired), событие Cancelled, напоминания Skipped; публикуется appointment-cancelled (Reason = Expired).
  2. Инвестор и агент уведомляются; повторный проход worker-а над уже не-Pending записью — no-op (ровно одно appointment-cancelled на запись). Завершается.

A3. Решение после дедлайна или по неактуальной записи

  1. Агент подтверждает/отклоняет запись после PendingExpiresAt, либо запись уже переведена worker-ом в Expired или отменена инвестором → отказ APPOINTMENT_INVALID_STATE (409). Клиент перечитывает запись и показывает актуальный статус.

A4. Гонка «агент ↔ инвестор ↔ worker»

  1. Подтверждение агента конкурирует с отменой инвестора (UC-CAL-005) и таймаутом worker-а: побеждает первая зафиксированная мутация (оптимистическая блокировка по версии строки), проигравший получает APPOINTMENT_INVALID_STATE и перечитывает состояние. Каскады выполняются ровно один раз — победителем.

A5. Запись на слот с auto-confirm

  1. Для слотов AutoConfirm = true сценарий не возникает: запись создаётся сразу Confirmed (PendingExpiresAt = null), решение агента не требуется, appointment-confirmed публикуется с AutoConfirmed = true уже при бронировании.

Постусловие

  • Запись — в одном из статусов: Confirmed (встреча состоится; слот Booked, событие Planned), Declined или Expired (слот освобождён или истёк, событие Cancelled, напоминания Skipped).
  • По каждому переходу опубликовано ровно одно интеграционное событие (appointment-confirmed / appointment-cancelled); лента лида CRM и уведомления пополнены асинхронно.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
APPOINTMENT_NOT_FOUND404Запись не найдена или не на слот актораОбновить список записей
APPOINTMENT_ACCESS_DENIED403Нет calendar.appointments.decideСкрыть действия решения
VALIDATION_FAILED400Отклонение без CancelReason (A1)Запросить причину
APPOINTMENT_INVALID_STATE409Запись не Pending; решение после PendingExpiresAt; проигранная гонка (A3, A4)Перечитать запись

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
AppointmentConfirmedEvent.v1pin.calendar.appointment-confirmed.v1Pending → Confirmed (шаг 4)crm (лента лида), notifications (AppointmentConfirmed)
AppointmentCancelledEvent.v1pin.calendar.appointment-cancelled.v1Pending → Declined (A1, Reason = DeclinedByAgent); Pending → Expired (A2, Reason = Expired)crm (лента лида), notifications (AppointmentCancelled)
CalendarEventStatusChangedEvent.v1pin.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)
AC4Worker переводит просроченную 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.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные Документы

  • 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 (исход подтверждённой встречи).