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.
| Поле | Значение |
|---|
| Сервис/контекст | calendar |
| API area | agent |
| Feature group | calendar-events |
| Статус | draft |
| Документ | docs/06-services/calendar/api/agent/cancel-event.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (агент агентства / консультант ПИН — владелец календаря) |
| Auth policy / Permissions | calendar.events.manage |
| Scope (RBAC) | организация: компания актора (агентство / ПИН) |
| Record-level | только владелец: OwnerUserId = ActorUserId; чужое событие того же tenant → CAL_EVENT_ACCESS_DENIED |
| Tenant isolation | TenantId из токена; чужой tenant → CAL_EVENT_NOT_FOUND |
| Audit | update: UpdatedByUserId = ActorUserId, UpdatedAt; CancelReason — постоянный след причины; история статусов — события pin.calendar.event-status-changed.v1 |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/agent/calendar/events/{id}/cancel |
| Success status | 200 |
| Idempotency header | нет (повтор над отменённым событием → CAL_EVENT_ALREADY_TERMINAL, состояние не портится) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ActorUserId | токен (sub) | Guid | Да | Required | record-level проверка владельца |
TenantId | токен (tenant_id) | Guid | Да | Required | Tenant isolation |
Id | route | Guid | Да | валидный Guid | — |
CancelReason | body | string | Да | непустая строка, ≤ 512 символов; отсутствие/пустая → CAL_EVENT_CANCEL_REASON_REQUIRED (INV-CAL-7) | может содержать ПДн: в логи не пишется |
Модель ответа — CalendarEventModel
200 с событием после перехода — единичная CalendarEventModel (дом — list-events.md):
Status = Cancelled, заполненный CancelReason, инкрементированная Version. Отменённое событие перестаёт
участвовать в расчёте конфликтов (INV-CAL-6 учитывает только Planned/InProgress).
Алгоритм
- Контекст и доступ. Актор — владелец; требуется
calendar.events.manage.
- Проверка входа.
CancelReason непустой и ≤ 512 символов; отсутствует/пустой →
CAL_EVENT_CANCEL_REASON_REQUIRED; превышение длины → ValidationError.
- Загрузка и принадлежность. Событие по
Id в tenant актора: не найдено/чужой tenant →
CAL_EVENT_NOT_FOUND; другой владелец → CAL_EVENT_ACCESS_DENIED.
- Guards происхождения (запрет
Origin ≠ Manual). Origin = Appointment →
CAL_EVENT_MANAGED_BY_APPOINTMENT (INV-CAL-3: отмена — только через операции записи, каскад по таблице
appointment.md сам переведёт событие в Cancelled с причиной из записи);
Origin = CrmBooking → CAL_EVENT_MANAGED_BY_CRM_BOOKING (INV-CAL-4: дедлайн отменяет только consumer
при завершении/отмене брони).
- Guard статуса (INV-CAL-2/7). Допустимые исходные статусы —
Planned и InProgress;
Completed/Cancelled → CAL_EVENT_ALREADY_TERMINAL.
- Переход. В одном согласованном изменении:
Status = Cancelled, CancelReason сохранён; все
Pending-reminder-строки → Skipped (правило 3 event-reminder.md);
публикация pin.calendar.event-status-changed.v1 (Status = Cancelled, CancelReason).
- Результат.
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 |
|---|
ValidationError | 400 | Id не Guid; CancelReason длиннее 512 символов | «Некорректные параметры отмены» | no retry |
CAL_EVENT_CANCEL_REASON_REQUIRED | 400 | CancelReason отсутствует или пустой (INV-CAL-7) | «Укажите причину отмены» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | — | no retry |
FORBIDDEN | 403 | нет права calendar.events.manage | — | no retry |
CAL_EVENT_ACCESS_DENIED | 403 | событие другого владельца в tenant актора | «Нет доступа к событию» | no retry |
CAL_EVENT_NOT_FOUND | 404 | событие не существует / чужой tenant | «Событие не найдено» | no retry |
CAL_EVENT_MANAGED_BY_APPOINTMENT | 409 | Origin = Appointment (INV-CAL-3) — отменяйте запись, событие отменится каскадом | «Событие управляется записью» | no retry |
CAL_EVENT_MANAGED_BY_CRM_BOOKING | 409 | Origin = CrmBooking (INV-CAL-4) — дедлайн снимает consumer booking-событий | «Событие управляется бронью CRM» | no retry |
CAL_EVENT_ALREADY_TERMINAL | 409 | статус уже 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-changed — ContractVersion = 1, additive-эволюция.
- Logs:
TenantId, OwnerUserId, EventId, переход → Cancelled, число погашенных напоминаний (без
текста CancelReason); metrics: счётчик отмен по Kind; traces — сквозной traceparent.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы