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

POST /api/v1/agent/calendar/appointments/{appointmentId}/decline — Отклонение записи

Назначение

Агент отклоняет ожидающую запись инвестора с обязательной причиной (Pending → Declined): встреча не состоится, инвестор получает уведомление с причиной. Каскадом слот освобождается и возвращается на витрину (если до начала ещё остаётся lead time), а порождённое календарное событие отменяется. Отклонить можно только Pending-запись; для подтверждённой используется отмена (cancel-appointment.md).

Metadata

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

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/agent/calendar/appointments/{appointmentId}/decline
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 = Declined, CancelReason = Reason, инкрементирован Version.

Алгоритм

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

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

ТипДетали
Интеграционные событияpin.calendar.appointment-cancelled.v1 (Reason = DeclinedByAgent), 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запись не Pending (в т.ч. Confirmed или терминальная), либо проигрыш конкурентной гонки«Запись уже в другом состоянии»no retry (перечитать состояние)

Постусловия

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

Acceptance Criteria

  • AC-1 (happy path). Given Pending-запись, до StartAt больше MinLeadTimeMinutes слота. When агент отклоняет с причиной. Then 200: запись Declined, слот Available (виден на витрине), событие Cancelled, напоминания Skipped; опубликовано appointment-cancelled (Reason = DeclinedByAgent).
  • AC-2 (поздний lead time). Given Pending-запись, до StartAt меньше MinLeadTimeMinutes. When отклонение. Then запись Declined, слот Expired (на витрину не возвращается).
  • AC-3 (без причины). Given Pending-запись. When decline с пустым Reason. Then 400 VALIDATION_FAILED; запись не изменилась.
  • AC-4 (подтверждённая). Given Confirmed-запись. When decline. Then 409 APPOINTMENT_INVALID_STATE (для подтверждённой — cancel-appointment.md).
  • AC-5 (гонка с таймаутом). Given Pending-запись на грани PendingExpiresAt, worker и decline конкурируют. Then побеждает ровно один; итог — либо Declined (Reason = DeclinedByAgent), либо Expired (Reason = Expired), но не оба; событий appointment-cancelled — ровно одно.
  • AC-6 (чужая запись). Given запись к другому агенту. When decline. Then 404 APPOINTMENT_NOT_FOUND.

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

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

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

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

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