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

POST /api/v1/agent/calendar/events/{id}/complete — Завершение события (→ Completed)

Назначение

Владелец календаря фиксирует, что событие состоялось/выполнено: показ проведён, задача закрыта. Переход допустим из Planned (завершение без явного start) и из InProgress; статус становится терминальным Completed, неотправленные напоминания гасятся (Pending → Skipped), публикуется pin.calendar.event-status-changed.v1. Исход встречи по записи (Completed/NoShow) фиксируется не здесь, а операцией complete-appointment.md — она каскадно завершает и событие.

Metadata

ПолеЗначение
Сервис/контекстcalendar
API areaagent
Feature groupcalendar-events
Статусdraft
Документdocs/06-services/calendar/api/agent/complete-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; история статусов — события pin.calendar.event-status-changed.v1

HTTP-контракт

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

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

Body отсутствует — переход определяется маршрутом.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdтокен (sub)GuidДаRequiredrecord-level проверка владельца
TenantIdтокен (tenant_id)GuidДаRequiredTenant isolation
IdrouteGuidДавалидный Guid

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

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

Алгоритм

  1. Контекст и доступ. Актор — владелец; требуется calendar.events.manage.
  2. Загрузка и принадлежность. Событие по Id в tenant актора: не найдено/чужой tenant → CAL_EVENT_NOT_FOUND; другой владелец → CAL_EVENT_ACCESS_DENIED.
  3. Guards происхождения. Origin = AppointmentCAL_EVENT_MANAGED_BY_APPOINTMENT (INV-CAL-3: исход встречи фиксируется операцией записи complete-appointment.md, каскад завершает событие); Origin = CrmBookingCAL_EVENT_MANAGED_BY_CRM_BOOKING (INV-CAL-4: дедлайн закрывает consumer по событиям брони).
  4. Guard статуса (INV-CAL-2). Допустимые исходные статусы — Planned и InProgress (матрица переходов); Completed/CancelledCAL_EVENT_ALREADY_TERMINAL. Отношение к StartAt/EndAt не проверяется: владелец вправе закрыть событие раньше или позже планового времени.
  5. Переход. В одном согласованном изменении: Status = Completed; все Pending-reminder-строки → Skipped (правило 3 event-reminder.md); публикация pin.calendar.event-status-changed.v1 (Status = Completed).
  6. Результат. 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«Некорректный идентификатор»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)«Событие управляется бронью CRM»no retry
CAL_EVENT_ALREADY_TERMINAL409статус уже Completed/Cancelled (INV-CAL-7)«Событие завершено или отменено»no retry

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

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

Acceptance-критерии

  • Из InProgress. Дано: событие InProgress. Когда: POST …/complete. Тогда: 200, Status = Completed, опубликовано event-status-changed.
  • Из Planned (без start). Дано: событие Planned. Тогда: 200, Status = Completed — явный start не обязателен (матрица допускает Planned → Completed).
  • Напоминания погашены. Дано: событие с Pending-строкой (DueAt в будущем). Когда: завершение. Тогда: строка Skipped, reminder-due по ней не публикуется.
  • Повторное завершение. Дано: событие Completed. Тогда: 409 CAL_EVENT_ALREADY_TERMINAL; второе event-status-changed не публикуется.
  • Событие записи. Дано: Origin = Appointment. Тогда: 409 CAL_EVENT_MANAGED_BY_APPOINTMENT — исход (Completed/NoShow) фиксируется операцией записи, каскад завершает событие.
  • Дедлайн брони. Дано: Origin = CrmBooking. Тогда: 409 CAL_EVENT_MANAGED_BY_CRM_BOOKING.
  • Гонка с worker-ом/consumer-ом. Дано: параллельные complete владельца и изменение worker-а/consumer-а. Тогда: побеждает один; проигравший получает 409 по актуальному состоянию.

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

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

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

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

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

  • Матрица переходов INV-CAL-2/7: calendar-event.md; гашение напоминаний: event-reminder.md.
  • Дом модели: list-events.md; начало: start-event.md; отмена: cancel-event.md.
  • Исход встречи по записи: appointment.md (операция complete-appointment.md, Outcome = Completed | NoShow, каскад на событие).
  • Контракт события: ../../events/event-status-changed.md.