UC-CAL-005. Отмена и перенос записи
Назначение
Описывает изменение договорённости о встрече: инвестор отменяет свою запись или переносит её на другой слот
(в пределах cutoff-окна слота), агент отменяет запись со своей стороны (cutoff его не ограничивает).
Перенос — не правка времени, а атомарная пара «отмена текущей записи + бронирование нового слота» со
взаимными ссылками; при отказе любой половины не происходит ничего. Каждая отмена каскадно освобождает слот
и отменяет календарное событие агента. Связан с контекстом calendar.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CAL-005 |
| Контекст | calendar (см. docs/06-services/calendar/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор (отмена/перенос своей записи) — основной; Агент (отмена записи на свой слот) — альтернативный поток A3 |
| Permissions / scope | Инвестор — record-level по InvestorUserId, без permission-модели tenant-а; агент — calendar.appointments.decide, scope Organization |
| Tenant | Инвестор — вне tenant (ADR-0052); запись живёт в tenant компании агента |
| Record-level | Инвестор управляет только своими записями; агент — только записями на свои слоты |
Предусловие
- Существует активная запись (
PendingилиConfirmed) — UC-CAL-003 / UC-CAL-004. - Для действий инвестора: cutoff не пройден —
now ≤ StartAt − CancellationCutoffMinutes(параметр слота, default 120 минут). - Для переноса: на витрине есть другой подходящий свободный слот.
Триггер
| Поле | Значение |
|---|---|
| Источник | UI button |
| Триггерящий элемент | ЛК инвестора → «Мои записи» → «Отменить» / «Перенести»; ЛК агента → «Записи» → «Отменить» |
Основной Поток
- Инвестор открывает «Мои записи» (
../api/investor/list-my-appointments.md) и карточку записи (../api/investor/get-my-appointment.md): время, вид, агент, статус, до какого момента возможна отмена/перенос (StartAt − CancellationCutoffMinutes). - Инвестор нажимает «Отменить»; клиент вызывает
../api/investor/cancel-my-appointment.md. - Система проверяет: запись принадлежит инвестору (record-level), статус активный (
Pending/Confirmed), cutoff не пройден (INV-APT-3, иначеAPPOINTMENT_CUTOFF_PASSED, A2). - В одном согласованном изменении: запись →
Cancelled(CancelledBy = Investor); слот освобождается (Booked → Available, еслиnow ≤ StartAt − MinLeadTimeMinutes, иначеBooked → Expired); календарное событие агента →Cancelled, егоPending-напоминания →Skipped; публикуетсяpin.calendar.appointment-cancelled.v1(Reason = CancelledByInvestor). - Асинхронно:
crmпишет в ленту лида «встреча отменена клиентом»;notificationsуведомляет агента. Освобождённый слот снова виден на витрине. - Инвестор видит запись в статусе «отменена»; активные лимиты (INV-APT-2) высвобождены — можно записаться заново.
Альтернативные Потоки
A1. Перенос записи инвестором
- Инвестор нажимает «Перенести», выбирает новый свободный слот (того же вида встречи) и вызывает
../api/investor/reschedule-my-appointment.md. - Система проверяет guard-ы обеих половин одновременно: cutoff текущей записи (INV-APT-3) и
доступность нового слота (статус
Available, lead time — INV-SLT-3/5); антиспам-лимиты INV-APT-2 считаются с учётом атомарной отмены старой записи (перенос не увеличивает число активных записей). - В одном согласованном изменении: старая запись →
Cancelled(CancelledBy = Investor,RescheduledToAppointmentId= новая) с полным каскадом отмены (шаг 4 основного потока); новый слотAvailable → Booked; создаётся новая запись (RescheduledFromAppointmentId= старая) со статусом поAutoConfirmнового слота и новое календарное событие; публикуютсяappointment-cancelled (CancelledByInvestor, RescheduledToAppointmentId)иappointment-created(+appointment-confirmedпри auto-confirm). - Если новый слот занят (гонка) —
SLOT_ALREADY_BOOKED, если cutoff старой записи пройден —APPOINTMENT_CUTOFF_PASSED: ничего не меняется, старая запись и её слот не тронуты. Инвестор выбирает другой слот или оставляет запись. Возвращается к шагу 1 или завершается.
A2. Cutoff пройден
- До начала встречи меньше
CancellationCutoffMinutes→ отказAPPOINTMENT_CUTOFF_PASSED(409). Клиент объясняет, что отмена самостоятельно уже невозможна, и предлагает связаться с агентом — агент отменить может (A3). Завершается.
A3. Отмена агентом
- Агент отменяет активную запись (
../api/agent/cancel-appointment.md) с обязательной причиной (CancelReason, иначеVALIDATION_FAILED); cutoff агента не ограничивает, но отмена возможна только до фиксации исхода (UC-CAL-006). - Каскад тот же, что на шаге 4;
CancelledBy = Agent, публикуетсяappointment-cancelled (Reason = CancelledByAgent); инвестор уведомляется. Завершается.
A4. Запись уже неактивна
- Запись terminal (
Declined/Cancelled/Expired/Completed/NoShow) — например, worker успел раньше или другая сторона уже отменила → отказAPPOINTMENT_INVALID_STATE(409). Клиент перечитывает запись и показывает актуальный статус.
A5. Конкурентные действия сторон
- Отмена инвестора конкурирует с подтверждением/отменой агента и таймаутом worker-а: побеждает первая
зафиксированная мутация (оптимистическая блокировка), проигравший получает
APPOINTMENT_INVALID_STATE. Каскад на слот и событие выполняется ровно один раз.
Постусловие
- Отмена: запись
Cancelledс заполненнымиCancelledBy(иCancelReasonпри отмене агентом); слот освобождён (Available) или истёк (Expired); календарное событиеCancelled, напоминанияSkipped. - Перенос: старая запись
Cancelledи новая активная запись связаны взаимными ссылкамиRescheduledFrom/To(цепочка переносов восстановима); новый слотBooked, новое событиеPlanned. - По каждому переходу опубликованы соответствующие события
pin.calendar.appointment-*; лента лида CRM и уведомления пополнены асинхронно.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
APPOINTMENT_NOT_FOUND | 404 | Запись не найдена или принадлежит другому инвестору/агенту | Обновить список записей |
APPOINTMENT_ACCESS_DENIED | 403 | Агент без calendar.appointments.decide (A3) | Скрыть действие |
VALIDATION_FAILED | 400 | Отмена агентом без CancelReason (A3) | Запросить причину |
APPOINTMENT_CUTOFF_PASSED | 409 | Отмена/перенос инвестором позже StartAt − CancellationCutoffMinutes (A2) | Предложить связаться с агентом |
APPOINTMENT_INVALID_STATE | 409 | Запись неактивна; проигранная гонка (A4, A5) | Перечитать запись |
SLOT_ALREADY_BOOKED | 409 | Новый слот переноса занят (A1) | Предложить другой слот |
CAL_SLOT_NOT_AVAILABLE | 409 | Новый слот переноса истёк/заархивирован (A1) | Обновить витрину |
CAL_SLOT_LEAD_TIME_VIOLATION | 409 | До нового слота меньше MinLeadTimeMinutes (A1) | Предложить более поздний слот |
APPOINTMENT_DUPLICATE | 409 | Перенос нарушил бы антиспам-лимиты INV-APT-2 (A1) | Показать активные записи |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
AppointmentCancelledEvent.v1 | pin.calendar.appointment-cancelled.v1 | Отмена инвестором (шаг 4, CancelledByInvestor; при переносе — с RescheduledToAppointmentId), отмена агентом (A3, CancelledByAgent) | crm (лента лида), notifications (AppointmentCancelled) |
AppointmentCreatedEvent.v1 | pin.calendar.appointment-created.v1 | Новая запись при переносе (A1) | crm, notifications |
AppointmentConfirmedEvent.v1 | pin.calendar.appointment-confirmed.v1 | Auto-confirm новой записи при переносе (A1) | crm, notifications |
CalendarEventStatusChangedEvent.v1 / CalendarEventCreatedEvent.v1 | pin.calendar.event-status-changed.v1 / pin.calendar.event-created.v1 | Каскадная отмена старого события; создание нового при переносе | notifications |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Отмена инвестором в пределах cutoff атомарно: запись Cancelled (CancelledBy = Investor), слот освобождён, событие Cancelled, напоминания Skipped, опубликовано appointment-cancelled (CancelledByInvestor) | интеграционный тест отмены |
| AC2 | Отмена за 60 минут до встречи при CancellationCutoffMinutes = 120 → APPOINTMENT_CUTOFF_PASSED; агент ту же запись отменить может (A2, A3) | негативный тест cutoff |
| AC3 | Перенос атомарен: старая запись Cancelled с RescheduledToAppointmentId, старый слот освобождён, новая запись с RescheduledFromAppointmentId на Booked-слоте, статус — по AutoConfirm нового слота | интеграционный тест переноса (A1) |
| AC4 | Занятость нового слота при переносе → SLOT_ALREADY_BOOKED, старая запись и слот не изменены (всё-или-ничего) | тест отказа половины пары (A1) |
| AC5 | Отмена агентом без причины → VALIDATION_FAILED; с причиной — каскад и appointment-cancelled (CancelledByAgent) | тест отмены агентом (A3) |
| AC6 | Слот, освобождённый позже StartAt − MinLeadTimeMinutes, переходит в Expired, а не Available (повторно не продаётся) | тест правила освобождения |
| AC7 | Отмена уже терминальной записи → APPOINTMENT_INVALID_STATE; конкурентные отмены обеих сторон дают ровно один каскад | тест гонки (A4, A5) |
| AC8 | После отмены лимиты INV-APT-2 инвестора высвобождены — новая запись к тому же агенту проходит | тест высвобождения лимитов |
Наблюдаемость
- Логи (без PII).
appointment-cancelled(appointmentId,slotId,cancelledBy,reason?— длина, не текст),appointment-rescheduled(fromAppointmentId,toAppointmentId,newSlotId). - Метрики.
calendar_appointments_cancelled_total{cancelled_by};calendar_appointments_rescheduled_total{result};calendar_cutoff_rejections_total(спрос на отмену после cutoff — сигнал пересмотреть default).
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (6): UC-CAL-001. Агент ведёт рабочий календарь, UC-CAL-002. Агент публикует слоты доступности, UC-CAL-003. Инвестор бронирует слот с витрины, UC-CAL-004. Агент подтверждает или отклоняет запись, UC-CAL-006. Агент фиксирует исход встречи, US-реестр контекста calendar (US-CAL)
- scenarios (1): UC-OVR-011. Самозапись инвестора: от слота до исхода встречи и лида в воронке
- index.md (1): Calendar Service
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- Domain: appointment.md (INV-APT-3/5, атомарная пара переноса, каскад-таблица), availability-slot.md (правило освобождения слота), calendar-event.md, event-reminder.md.
- API:
../api/investor/list-my-appointments.md,../api/investor/get-my-appointment.md,../api/investor/cancel-my-appointment.md,../api/investor/reschedule-my-appointment.md,../api/agent/cancel-appointment.md. - События:
../events/appointment-cancelled.md,../events/appointment-created.md,../events/appointment-confirmed.md. - Связанные UC: UC-CAL-003 (бронирование, гонка за слот),
UC-CAL-004 (таймаут
Pending), UC-CAL-006 (после исхода отмена невозможна).