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

POST /api/v1/agent/calendar/appointments/{appointmentId}/confirm — Подтверждение записи

Назначение

Агент подтверждает ожидающую запись инвестора (Pending → Confirmed): встреча состоится, инвестор получает уведомление, дедлайн решения (PendingExpiresAt) снимается. Повторный вызов по уже подтверждённой записи — безопасный no-op. Записи со слотов с AutoConfirm = true подтверждаются автоматически при бронировании и через эту операцию не проходят.

Metadata

ПолеЗначение
Сервис/контекстcalendar
API areaagent
Feature groupappointments
Статусdraft
Документdocs/06-services/calendar/api/agent/confirm-appointment.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (агент агентства / консультант ПИН — сторона записи)
Auth policy / Permissionscalendar.appointments.decide
Scope (RBAC)организация — компания актора (агентство / ПИН)
Record-levelрешения — только по собственным записям (AgentUserId = actor); чужая запись — APPOINTMENT_NOT_FOUND (fail-closed)
Tenant isolationTenantId из токена (ADR-0052)
Auditupdate: ConfirmedAt, UpdatedByUserId = actor; история — событие appointment-confirmed

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/agent/calendar/appointments/{appointmentId}/confirm
Success status200 (обновлённая запись; в т.ч. no-op повтора)
Idempotency headerне применяется: операция естественно идемпотентна — повтор по Confirmed-записи возвращает 200 без изменений и без повторного события
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdтокен (sub)GuidДаRequiredrecord-level
TenantIdтокен (tenant_id)GuidДаRequiredTenant isolation
AppointmentIdrouteGuidДаRequired; валидный Guid

Тело запроса отсутствует.

Модель ответа

Обновлённая запись — AgentAppointmentModel (дом — list-appointments.md): Status = Confirmed, заполнен ConfirmedAt, инкрементирован Version.

Алгоритм

  1. Контекст и доступ. Актор с permission calendar.appointments.decide (иначе APPOINTMENT_ACCESS_DENIED); запись ищется среди записей актора в его tenant — не найдена/чужая → APPOINTMENT_NOT_FOUND.
  2. Идемпотентный повтор. Если запись уже Confirmed — no-op: 200 с текущим состоянием, каскады не выполняются, событие повторно не публикуется.
  3. Проверка состояния. Допустимый исходный статус — только Pending (матрица переходов — appointment.md). Терминальная запись (Declined/Cancelled/Expired/ Completed/NoShow) → APPOINTMENT_INVALID_STATE.
  4. Дедлайн решения. Подтверждение допустимо только пока now ≤ PendingExpiresAt; позже → APPOINTMENT_INVALID_STATE — запись обречена на Expired (worker ../../background/appointment-timeout.md), инвестору предлагается записаться заново.
  5. Переход. Pending → Confirmed: ConfirmedAt = now, UpdatedByUserId = actor. Каскадов на слот и календарное событие нет — слот остаётся Booked, событие остаётся Planned (таблица каскадов — appointment.md). Атомарно с переходом публикуется appointment-confirmed (AutoConfirmed = false).
  6. Конкуренция. Гонка «confirm ↔ отмена инвестором ↔ таймаут worker-а» разрешается версией записи: победитель фиксирует переход, проигравший получает APPOINTMENT_INVALID_STATE и перечитывает актуальное состояние.
  7. Результат. 200 + обновлённая AgentAppointmentModel.

Побочные эффекты

ТипДетали
Интеграционные событияpin.calendar.appointment-confirmed.v1, key = AppointmentId (../../events/appointment-confirmed.md); публикация атомарна с переходом
Проекции downstreamcrm — запись в ленту лида (../../../crm/consumers/calendar-appointments.md)
Уведомленияnotifications консюмит событие и уведомляет инвестора (AppointmentConfirmed, ../../../notifications/consumers/calendar-events.md)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400AppointmentId — не валидный Guid«Некорректный идентификатор»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
APPOINTMENT_ACCESS_DENIED403нет permission calendar.appointments.decide«Недостаточно прав»no retry
APPOINTMENT_NOT_FOUND404запись не существует или принадлежит другому агенту/tenant«Запись не найдена»no retry
APPOINTMENT_INVALID_STATE409запись не Pending и не Confirmed (терминальная), либо now > PendingExpiresAt, либо проигрыш конкурентной гонки«Запись уже в другом состоянии»no retry (перечитать состояние)

Постусловия

После 200 (не-no-op): запись Confirmed с ConfirmedAt; слот остался Booked; календарное событие осталось Planned; напоминания не пересоздавались; опубликовано ровно одно appointment-confirmed. Downstream (лента лида CRM, уведомление инвестору) догоняет асинхронно — обычно < 1 мин.

Acceptance Criteria

  • AC-1 (happy path). Given Pending-запись, now ≤ PendingExpiresAt. When агент подтверждает. Then 200: запись Confirmed, ConfirmedAt заполнен, слот Booked, событие Planned; опубликовано appointment-confirmed (AutoConfirmed = false); инвестор получает уведомление.
  • AC-2 (идемпотентный повтор). Given запись уже Confirmed. When повторный confirm. Then 200 без изменений (Version не растёт), второе событие не публикуется.
  • AC-3 (просроченный дедлайн). Given Pending-запись, now > PendingExpiresAt, worker ещё не прошёл. When confirm. Then 409 APPOINTMENT_INVALID_STATE; запись остаётся Pending до прохода worker-а.
  • AC-4 (терминальная). Given запись Cancelled. When confirm. Then 409 APPOINTMENT_INVALID_STATE.
  • AC-5 (гонка с отменой инвестора). Given одновременные confirm агента и cancel-my-appointment инвестора. Then побеждает ровно один; проигравший получает 409 и при перечитке видит фактический статус.
  • AC-6 (чужая запись). Given запись к другому агенту. When confirm. Then 404 APPOINTMENT_NOT_FOUND.

Совместимость и наблюдаемость

  • Контракт стабилен: изменение семантики no-op-повтора или guard-а дедлайна — breaking для клиентов.
  • Logs: appointment confirmed (TenantId, AgentUserId, AppointmentId; без Comment); metrics: calendar_appointment_decisions_total{decision=confirmed}, время от создания до решения; traces — сквозной traceparent.

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

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

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