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

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

Назначение

Инвестор отменяет свою активную запись к агенту — не позже cutoff слота (за CancellationCutoffMinutes до начала, INV-APT-3). Отмена каскадно освобождает слот (он возвращается на витрину, если ещё пригоден для повторного бронирования) и отменяет календарное событие агента; агент узнаёт об отмене из уведомления, CRM — из события appointment-cancelled. После cutoff отмена инвестору недоступна — остаётся связаться с агентом (агент cutoff-ом не ограничен).

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Инвестор с JWT)
Auth policy / Permissionsnone (достаточно аутентификации инвестора)
Scope (RBAC)не применяется — инвестор вне tenant (ADR-0052)
Record-levelотменить можно только свою запись: InvestorUserId = ActorUserId из токена; чужая → 404
Auditupdate: CancelledBy = Investor, CancelReason, UpdatedByUserId = actor — иммутабельный след отмены

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/investor/calendar/appointments/{appointmentId}/cancel
Success status200
Idempotency headerне применяется: повторная отмена уже отменённой записи → 409 APPOINTMENT_INVALID_STATE (терминальный статус)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdтокен (sub)GuidДаRequiredrecord-level фильтр InvestorUserId
AppointmentIdrouteGuidДаRequired, валидный Guid
CancelReasonbodystring?Нет0..512; не задан → записывается стандартная причина «Отменено клиентом» (хранение требует непустой причины при Cancelled)может содержать ПДн — в логи не пишется

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

Обновлённая карточка записи — модель по дому list-my-appointments.md: Status = Cancelled, CancelledBy = Investor, CancelReason заполнен.

Алгоритм

  1. Контекст и доступ. Актор — аутентифицированный инвестор; запись ищется с условием InvestorUserId = actor.
  2. Проверка входа и предусловий. CancelReason ≤ 512 — иначе VALIDATION_FAILED. Запись не найдена или чужая → APPOINTMENT_NOT_FOUND (fail-closed). Статус ∉ {Pending, Confirmed} (терминальная) → APPOINTMENT_INVALID_STATE.
  3. Cutoff (INV-APT-3). now > StartAt − CancellationCutoffMinutes (параметр слота) → APPOINTMENT_CUTOFF_PASSED; запись не меняется.
  4. Что меняется (атомарный каскад, INV-APT-6). В одном согласованном изменении:
    • запись → Cancelled (CancelledBy = Investor, CancelReason — из запроса или стандартная);
    • слот Booked → Available, если ещё пригоден для повторного бронирования (now ≤ StartAt − MinLeadTimeMinutes), иначе Booked → Expired (правило каскадов — appointment.md);
    • календарное событие агента → Cancelled (единственный легальный канал — каскад записи, INV-CAL-3), его Pending-напоминания → Skipped (event-reminder.md);
    • публикуются appointment-cancelled (Reason = CancelledByInvestor) и event-status-changed — атомарно со сменой состояния. Конкуренция «инвестор ↔ агент ↔ worker» (cancel vs confirm vs timeout) разрешается версией записи: победитель фиксирует переход, проигравший перечитывает состояние и получает APPOINTMENT_INVALID_STATE.
  5. Результат. Обновлённая MyAppointmentModel → HTTP 200.

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

ТипДетали
Интеграционные событияAppointmentCancelledEventpin.calendar.appointment-cancelled.v1 (key = AppointmentId, Reason = CancelledByInvestor); CalendarEventStatusChangedEventpin.calendar.event-status-changed.v1 (событие агента → Cancelled)
Проекции / поискосвобождённый слот (Available) снова виден на витрине list-available-slots.md; Pending-напоминания события → Skipped
Уведомленияопосредованно: notifications — агенту «клиент отменил запись»; CRM — событие в ленту лида (../../../crm/consumers/calendar-appointments.md)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400AppointmentId невалиден; CancelReason > 512«Проверьте корректность полей»no retry
UNAUTHORIZED401нет/просрочен JWTno retry
APPOINTMENT_NOT_FOUND404запись не существует или принадлежит другому инвестору«Запись не найдена»no retry
APPOINTMENT_CUTOFF_PASSED409now > StartAt − CancellationCutoffMinutes (INV-APT-3)«Срок отмены истёк — свяжитесь с агентом»no retry
APPOINTMENT_INVALID_STATE409запись уже терминальна (Declined/Cancelled/Expired/Completed/NoShow), в т.ч. проигранная гонка«Запись уже не активна»no retry (перечитать карточку)

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

  • Контракт стабилен: CancelReason — additive optional; коды 409 — часть публичного поведения клиента (по ним различаются «поздно» и «уже не активна»).
  • Logs: cancel-appointment received/cutoff-rejected/cancelled (AppointmentId, InvestorUserId, освобождение слота Available/Expired; CancelReason не пишется); metrics: calendar_appointment_cancellations_total{by="investor", result}; traces — сквозной traceparent.

Постусловия

После 200 гарантированно: запись Cancelled (CancelledBy = Investor); слот освобождён (Available) либо списан (Expired) по правилу lead-time; календарное событие агента Cancelled, напоминания по нему не уйдут; опубликованы appointment-cancelled и event-status-changed. Асинхронно: агент получает уведомление, CRM дописывает ленту лида (обычно < 1 мин).

Acceptance Criteria

  • AC-1 (happy). Дано: Confirmed-запись, до StartAt 3 часа, cutoff 120 мин. Когда: инвестор отменяет. Тогда: 200, запись Cancelled (CancelledBy = Investor), слот снова Available на витрине, событие агента Cancelled, опубликовано appointment-cancelled (Reason = CancelledByInvestor).
  • AC-2 (cutoff). Дано: до StartAt 60 минут, cutoff 120 мин. Тогда: 409 APPOINTMENT_CUTOFF_PASSED, запись и слот не изменились.
  • AC-3 (слот уже не продать). Дано: отмена в пределах cutoff, но now > StartAt − MinLeadTimeMinutes. Тогда: запись Cancelled, слот → Expired (на витрину не возвращается).
  • AC-4 (без причины). Дано: отмена без CancelReason. Тогда: 200, в записи — стандартная причина «Отменено клиентом».
  • AC-5 (повторная отмена). Дано: запись уже Cancelled. Тогда: 409 APPOINTMENT_INVALID_STATE.
  • AC-6 (гонка с worker-ом). Дано: одновременно отмена инвестора и таймаут Pending (worker). Тогда: проходит ровно один переход; проигравший получает APPOINTMENT_INVALID_STATE; опубликовано ровно одно appointment-cancelled.

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

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

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

  • Сущность, матрица переходов, каскады: appointment.md (INV-APT-3/6); слот и правило освобождения: availability-slot.md; событие агента: calendar-event.md (INV-CAL-3); напоминания: event-reminder.md.
  • Карточка с echo cutoff: get-my-appointment.md; перенос вместо отмены: reschedule-my-appointment.md; отмена агентом — ../agent/cancel-appointment.md (без cutoff-ограничения).
  • Контракт события: ../../events/appointment-cancelled.md; таймаут Pending../../background/appointment-timeout.md.