POST /api/v1/agent/calendar/appointments/{appointmentId}/confirm — Подтверждение записи
Назначение
Агент подтверждает ожидающую запись инвестора (Pending → Confirmed): встреча состоится, инвестор получает
уведомление, дедлайн решения (PendingExpiresAt) снимается. Повторный вызов по уже подтверждённой записи —
безопасный no-op. Записи со слотов с AutoConfirm = true подтверждаются автоматически при бронировании и
через эту операцию не проходят.
| Поле | Значение |
|---|
| Сервис/контекст | calendar |
| API area | agent |
| Feature group | appointments |
| Статус | draft |
| Документ | docs/06-services/calendar/api/agent/confirm-appointment.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (агент агентства / консультант ПИН — сторона записи) |
| Auth policy / Permissions | calendar.appointments.decide |
| Scope (RBAC) | организация — компания актора (агентство / ПИН) |
| Record-level | решения — только по собственным записям (AgentUserId = actor); чужая запись — APPOINTMENT_NOT_FOUND (fail-closed) |
| Tenant isolation | TenantId из токена (ADR-0052) |
| Audit | update: ConfirmedAt, UpdatedByUserId = actor; история — событие appointment-confirmed |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/agent/calendar/appointments/{appointmentId}/confirm |
| Success status | 200 (обновлённая запись; в т.ч. no-op повтора) |
| Idempotency header | не применяется: операция естественно идемпотентна — повтор по Confirmed-записи возвращает 200 без изменений и без повторного события |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ActorUserId | токен (sub) | Guid | Да | Required | record-level |
TenantId | токен (tenant_id) | Guid | Да | Required | Tenant isolation |
AppointmentId | route | Guid | Да | Required; валидный Guid | — |
Тело запроса отсутствует.
Модель ответа
Обновлённая запись — AgentAppointmentModel (дом — list-appointments.md):
Status = Confirmed, заполнен ConfirmedAt, инкрементирован Version.
Алгоритм
- Контекст и доступ. Актор с permission
calendar.appointments.decide (иначе
APPOINTMENT_ACCESS_DENIED); запись ищется среди записей актора в его tenant — не найдена/чужая →
APPOINTMENT_NOT_FOUND.
- Идемпотентный повтор. Если запись уже
Confirmed — no-op: 200 с текущим состоянием, каскады не
выполняются, событие повторно не публикуется.
- Проверка состояния. Допустимый исходный статус — только
Pending (матрица переходов —
appointment.md). Терминальная запись (Declined/Cancelled/Expired/
Completed/NoShow) → APPOINTMENT_INVALID_STATE.
- Дедлайн решения. Подтверждение допустимо только пока
now ≤ PendingExpiresAt; позже →
APPOINTMENT_INVALID_STATE — запись обречена на Expired (worker
../../background/appointment-timeout.md), инвестору предлагается записаться заново.
- Переход.
Pending → Confirmed: ConfirmedAt = now, UpdatedByUserId = actor. Каскадов на слот и
календарное событие нет — слот остаётся Booked, событие остаётся Planned (таблица каскадов —
appointment.md). Атомарно с переходом публикуется
appointment-confirmed (AutoConfirmed = false).
- Конкуренция. Гонка «confirm ↔ отмена инвестором ↔ таймаут worker-а» разрешается версией записи:
победитель фиксирует переход, проигравший получает
APPOINTMENT_INVALID_STATE и перечитывает актуальное
состояние.
- Результат.
200 + обновлённая AgentAppointmentModel.
Побочные эффекты
| Тип | Детали |
|---|
| Интеграционные события | pin.calendar.appointment-confirmed.v1, key = AppointmentId (../../events/appointment-confirmed.md); публикация атомарна с переходом |
| Проекции downstream | crm — запись в ленту лида (../../../crm/consumers/calendar-appointments.md) |
| Уведомления | notifications консюмит событие и уведомляет инвестора (AppointmentConfirmed, ../../../notifications/consumers/calendar-events.md) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | AppointmentId — не валидный Guid | «Некорректный идентификатор» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
APPOINTMENT_ACCESS_DENIED | 403 | нет permission calendar.appointments.decide | «Недостаточно прав» | no retry |
APPOINTMENT_NOT_FOUND | 404 | запись не существует или принадлежит другому агенту/tenant | «Запись не найдена» | no retry |
APPOINTMENT_INVALID_STATE | 409 | запись не 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.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы