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

POST /api/v1/agent/calendar/events/{id}/cancel — Отмена события (→ Cancelled)

Назначение

Владелец календаря отменяет своё ручное событие с обязательной причиной: показ не состоится, задача потеряла смысл. Переход допустим из Planned и InProgress; статус становится терминальным Cancelled, неотправленные напоминания гасятся (Pending → Skipped), публикуется pin.calendar.event-status-changed.v1. Отменять можно только события Origin = Manual: встреча по записи отменяется операциями записи (cancel-appointment.md / decline-appointment.md — каскад отменит событие), дедлайн брони снимает consumer booking-событий CRM.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (агент агентства / консультант ПИН — владелец календаря)
Auth policy / Permissionscalendar.events.manage
Scope (RBAC)организация: компания актора (агентство / ПИН)
Record-levelтолько владелец: OwnerUserId = ActorUserId; чужое событие того же tenant → CAL_EVENT_ACCESS_DENIED
Tenant isolationTenantId из токена; чужой tenant → CAL_EVENT_NOT_FOUND
Auditupdate: UpdatedByUserId = ActorUserId, UpdatedAt; CancelReason — постоянный след причины; история статусов — события pin.calendar.event-status-changed.v1

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/agent/calendar/events/{id}/cancel
Success status200
Idempotency headerнет (повтор над отменённым событием → CAL_EVENT_ALREADY_TERMINAL, состояние не портится)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdтокен (sub)GuidДаRequiredrecord-level проверка владельца
TenantIdтокен (tenant_id)GuidДаRequiredTenant isolation
IdrouteGuidДавалидный Guid
CancelReasonbodystringДанепустая строка, ≤ 512 символов; отсутствие/пустая → CAL_EVENT_CANCEL_REASON_REQUIRED (INV-CAL-7)может содержать ПДн: в логи не пишется

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

200 с событием после перехода — единичная CalendarEventModel (дом — list-events.md): Status = Cancelled, заполненный CancelReason, инкрементированная Version. Отменённое событие перестаёт участвовать в расчёте конфликтов (INV-CAL-6 учитывает только Planned/InProgress).

Алгоритм

  1. Контекст и доступ. Актор — владелец; требуется calendar.events.manage.
  2. Проверка входа. CancelReason непустой и ≤ 512 символов; отсутствует/пустой → CAL_EVENT_CANCEL_REASON_REQUIRED; превышение длины → ValidationError.
  3. Загрузка и принадлежность. Событие по Id в tenant актора: не найдено/чужой tenant → CAL_EVENT_NOT_FOUND; другой владелец → CAL_EVENT_ACCESS_DENIED.
  4. Guards происхождения (запрет Origin ≠ Manual). Origin = AppointmentCAL_EVENT_MANAGED_BY_APPOINTMENT (INV-CAL-3: отмена — только через операции записи, каскад по таблице appointment.md сам переведёт событие в Cancelled с причиной из записи); Origin = CrmBookingCAL_EVENT_MANAGED_BY_CRM_BOOKING (INV-CAL-4: дедлайн отменяет только consumer при завершении/отмене брони).
  5. Guard статуса (INV-CAL-2/7). Допустимые исходные статусы — Planned и InProgress; Completed/CancelledCAL_EVENT_ALREADY_TERMINAL.
  6. Переход. В одном согласованном изменении: Status = Cancelled, CancelReason сохранён; все Pending-reminder-строки → Skipped (правило 3 event-reminder.md); публикация pin.calendar.event-status-changed.v1 (Status = Cancelled, CancelReason).
  7. Результат. 200 + обновлённая CalendarEventModel.

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

ТипДетали
Интеграционные событияpin.calendar.event-status-changed.v1, key = EventId, атомарно с переходом (контракт — ../../events/event-status-changed.md)
Уведомленияdownstream: notifications консюмит event-status-changed

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Id не Guid; CancelReason длиннее 512 символов«Некорректные параметры отмены»no retry
CAL_EVENT_CANCEL_REASON_REQUIRED400CancelReason отсутствует или пустой (INV-CAL-7)«Укажите причину отмены»no retry
UNAUTHORIZED401нет/просрочен JWTno retry
FORBIDDEN403нет права calendar.events.manageno retry
CAL_EVENT_ACCESS_DENIED403событие другого владельца в tenant актора«Нет доступа к событию»no retry
CAL_EVENT_NOT_FOUND404событие не существует / чужой tenant«Событие не найдено»no retry
CAL_EVENT_MANAGED_BY_APPOINTMENT409Origin = Appointment (INV-CAL-3) — отменяйте запись, событие отменится каскадом«Событие управляется записью»no retry
CAL_EVENT_MANAGED_BY_CRM_BOOKING409Origin = CrmBooking (INV-CAL-4) — дедлайн снимает consumer booking-событий«Событие управляется бронью CRM»no retry
CAL_EVENT_ALREADY_TERMINAL409статус уже Completed/Cancelled (INV-CAL-7)«Событие завершено или отменено»no retry

Предусловия и постусловия

  • Предусловия. Событие существует, принадлежит актору, Origin = Manual, статус Planned или InProgress.
  • Постусловия. Status = Cancelled (терминальный, INV-CAL-7), CancelReason сохранён; все Pending-напоминания — Skipped (reminder-due по ним не выйдет), Sent-строки нетронуты; опубликовано ровно одно pin.calendar.event-status-changed.v1 со Status = Cancelled и CancelReason.

Acceptance-критерии

  • Happy path. Дано: Planned-событие владельца. Когда: POST …/cancel с CancelReason = "Клиент перенёс отпуск". Тогда: 200, Status = Cancelled, CancelReason сохранён, Pending-напоминания → Skipped, опубликовано event-status-changed.
  • Из InProgress. Дано: событие InProgress. Тогда: отмена с причиной проходит (200, матрица допускает InProgress → Cancelled).
  • Без причины. Дано: POST без CancelReason (или с пустой строкой). Тогда: 400 CAL_EVENT_CANCEL_REASON_REQUIRED; событие не изменено.
  • Событие записи (запрет Origin ≠ Manual). Дано: событие Origin = Appointment. Тогда: 409 CAL_EVENT_MANAGED_BY_APPOINTMENT; после отмены самой записи (cancel-appointment.md) событие переходит в Cancelled каскадом.
  • Дедлайн брони. Дано: событие Origin = CrmBooking. Тогда: 409 CAL_EVENT_MANAGED_BY_CRM_BOOKING.
  • Повторная отмена. Дано: событие Cancelled. Тогда: 409 CAL_EVENT_ALREADY_TERMINAL; второе event-status-changed не публикуется.
  • Гонка с complete. Дано: параллельные cancel и complete. Тогда: побеждает один переход; проигравший — 409 CAL_EVENT_ALREADY_TERMINAL по актуальному состоянию.

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

  • Контракт запроса минимален (CancelReason); новые optional поля — additive. Контракт event-status-changedContractVersion = 1, additive-эволюция.
  • Logs: TenantId, OwnerUserId, EventId, переход → Cancelled, число погашенных напоминаний (без текста CancelReason); metrics: счётчик отмен по Kind; traces — сквозной traceparent.

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

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

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

  • Матрица переходов, INV-CAL-2/3/4/7 и таблица ошибок: calendar-event.md; гашение напоминаний: event-reminder.md.
  • Дом модели: list-events.md; смежные переходы: start-event.md, complete-event.md.
  • Отмена встречи по записи: appointment.md (операции cancel-appointment.md / decline-appointment.md, каскад на событие); отмена дедлайна брони — ../../consumers/crm-booking-events.md.
  • Контракт события: ../../events/event-status-changed.md.