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

POST /api/v1/agent/calendar/appointments/{appointmentId}/cancel — Отмена записи агентом

Назначение

Агент отменяет запись инвестора с обязательной причиной — как ожидающую (Pending), так и уже подтверждённую (Confirmed): встреча срывается по стороне агента (болезнь, форс-мажор, объект снят с показа). В отличие от инвестора, агент не связан cutoff-ом (INV-APT-3 ограничивает только инвестора) и может отменить вплоть до фиксации исхода. Каскадом слот освобождается (если ещё пригоден для повторного бронирования), календарное событие отменяется, инвестор получает уведомление с причиной.

Metadata

ПолеЗначение
Сервис/контекстcalendar
API areaagent
Feature groupappointments
Статусdraft
Документdocs/06-services/calendar/api/agent/cancel-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: CancelReason, CancelledBy = Agent, UpdatedByUserId = actor; история — событие appointment-cancelled

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/agent/calendar/appointments/{appointmentId}/cancel
Success status200 (обновлённая запись)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdтокен (sub)GuidДаRequiredrecord-level
TenantIdтокен (tenant_id)GuidДаRequiredTenant isolation
AppointmentIdrouteGuidДаRequired; валидный Guid
ReasonbodystringДа1..512 символов после trim; отсутствие/пустая строка → VALIDATION_FAILEDможет содержать ПДн — в логи не пишется

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

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

Алгоритм

  1. Контекст и доступ. Актор с permission calendar.appointments.decide (иначе APPOINTMENT_ACCESS_DENIED); запись ищется среди записей актора в его tenant — не найдена/чужая → APPOINTMENT_NOT_FOUND.
  2. Проверка входа. Reason непустой, ≤ 512 символов — иначе VALIDATION_FAILED (причина обязательна при Cancelled, INV-APT-6).
  3. Проверка состояния. Допустимые исходные статусы — Pending и Confirmed (матрица переходов — appointment.md); guard Confirmed → Cancelled — до фиксации исхода. Терминальная запись → APPOINTMENT_INVALID_STATE. Cutoff инвестора не проверяется — агент им не связан (INV-APT-3); после наступления StartAt состоявшуюся/несостоявшуюся встречу корректнее закрывать исходом (complete-appointment.md), но отмена остаётся допустимой (встреча сорвана до начала).
  4. Переход и каскады. В одном согласованном изменении (INV-APT-6):
    • запись Pending/Confirmed → Cancelled, CancelReason = Reason, CancelledBy = Agent;
    • слот: Booked → Available, если now ≤ StartAt − MinLeadTimeMinutes (возвращается на витрину), иначе Booked → Expired (правило освобождения — appointment.md);
    • календарное событие (Origin = Appointment): → Cancelled с CancelReason (INV-CAL-3), его Pending-напоминания → Skipped (event-reminder.md);
    • публикуется appointment-cancelled (Reason = CancelledByAgent).
  5. Конкуренция. Гонка «cancel агента ↔ отмена/перенос инвестором ↔ таймаут worker-а» — арбитраж версией записи; проигравший получает APPOINTMENT_INVALID_STATE и перечитывает состояние.
  6. Результат. 200 + обновлённая AgentAppointmentModel.

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

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

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Reason отсутствует, пустой или длиннее 512«Укажите причину отмены»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
APPOINTMENT_ACCESS_DENIED403нет permission calendar.appointments.decide«Недостаточно прав»no retry
APPOINTMENT_NOT_FOUND404запись не существует или принадлежит другому агенту/tenant«Запись не найдена»no retry
APPOINTMENT_INVALID_STATE409запись терминальна (Declined/Cancelled/Expired/Completed/NoShow), либо проигрыш конкурентной гонки«Запись уже в другом состоянии»no retry (перечитать состояние)

Постусловия

После 200: запись Cancelled (терминальна) с CancelReason и CancelledBy = Agent; слот Available (снова на витрине) либо Expired; календарное событие Cancelled, напоминания Skipped; опубликовано ровно одно appointment-cancelled (Reason = CancelledByAgent). Лента лида CRM и уведомление инвестору догоняют асинхронно.

Acceptance Criteria

  • AC-1 (подтверждённая, заранее). Given Confirmed-запись, до StartAt больше MinLeadTimeMinutes. When агент отменяет с причиной. Then 200: запись Cancelled (CancelledBy = Agent), слот Available, событие Cancelled, напоминания Skipped; опубликовано appointment-cancelled (Reason = CancelledByAgent); инвестор уведомлён.
  • AC-2 (в последний момент). Given Confirmed-запись, до StartAt 10 минут (CancellationCutoffMinutes = 120 — инвестор отменить уже не может). When агент отменяет. Then 200: cutoff агента не ограничивает; слот Expired (lead time прошёл).
  • AC-3 (ожидающая). Given Pending-запись. When агент отменяет с причиной. Then 200: запись Cancelled, слот освобождён по правилу lead time.
  • AC-4 (без причины). Given Confirmed-запись. When cancel с пустым Reason. Then 400 VALIDATION_FAILED.
  • AC-5 (после исхода). Given запись Completed. When cancel. Then 409 APPOINTMENT_INVALID_STATE.
  • AC-6 (гонка с инвестором). Given одновременные cancel агента и cancel-my-appointment инвестора. Then побеждает ровно один; CancelledBy и Reason события отражают победителя; событий appointment-cancelled — ровно одно.

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

  • Контракт стабилен; структурированный код причины — additive optional поле.
  • Logs: appointment cancelled by agent (TenantId, AgentUserId, AppointmentId, судьба слота; без Reason); metrics: calendar_appointment_decisions_total{decision=cancelled_by_agent}, calendar_slots_released_total{outcome}; traces — сквозной traceparent.

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

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

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