Cancellation Rules — Правила отмен и переносов
Извлечено из кода: backend/app/api/v1/endpoints/bookings.py, backend/app/api/v1/endpoints/reschedule.py, backend/app/services/telegram_handler.py, backend/app/models/booking.py
CR-001: Отмена записи провайдером через админку
- Описание: Провайдер может отменить запись через PATCH или DELETE
- Условие: PATCH /bookings/{id} с status="cancelled" ИЛИ DELETE /bookings/{id}
- Действие (PATCH):
- Поиск booking по id + business_id (авторизация)
- Обновление status = "cancelled"
- Уведомление клиента через Telegram (если подписан)
- Действие (DELETE):
- Если статус не "cancelled" — сначала ставит "cancelled" и уведомляет
- Затем hard delete из БД
- Исключения:
- PATCH не проверяет текущий статус — можно "отменить" уже завершённую запись
- DELETE — необратимое удаление (нет корзины, нет soft delete)
- Слот автоматически освобождается (запись удалена из busy intervals)
- Источник в коде:
backend/app/api/v1/endpoints/bookings.py::update_booking_status()backend/app/api/v1/endpoints/bookings.py::delete_booking()
CR-002: Отклонение записи через Telegram
- Описание: Провайдер отклоняет запись через inline-кнопку в Telegram
- Условие: Нажатие кнопки "Отклонить" (callback_data: reject_{booking_id})
- Действие:
- Проверка: booking.status == "pending" (только ожидающие)
- Проверка: business.telegram_chat_id == chat_id (авторизация)
- Статус -> "cancelled" (НЕ "rejected")
- Редактирование исходного сообщения (кнопки убираются)
- Уведомление клиенту: "Запись ОТМЕНЕНА"
- Исключения:
- Работает только из статуса "pending"
- Если запись уже обработана — ответ: "Запись уже обработана"
- Источник в коде:
backend/app/services/telegram_handler.py::_handle_confirm_reject()
CR-003: Отмена клиентом — НЕ реализована
- Описание: Клиент НЕ может самостоятельно отменить запись
- Условие: —
- Действие: TODO: не реализовано. Нет API endpoint для отмены клиентом. Нет Telegram-команды для отмены.
- Исключения: Клиент может только связаться с провайдером для отмены
- Источник в коде: Отсутствует
CR-004: Перенос записи провайдером через API
- Описание: Провайдер предлагает клиенту новое время через RescheduleProposal
- Условие: POST /reschedule/{booking_id}
- Действие:
- Проверка: booking принадлежит бизнесу пользователя
- Проверка: booking.status in ["pending", "confirmed"]
- Проверка: нет существующего PENDING proposal
- Создание RescheduleProposal:
- old_date, old_time: из текущего booking
- proposed_date, proposed_time: из запроса
- proposed_by: "PROVIDER_WEB"
- status: PENDING
- expires_at: now + 24 часа
- Уведомление клиенту (если подписан)
- Исключения:
- Только один PENDING proposal на booking (HTTP 409 при дубле)
- Нет проверки доступности предложенного слота через API
- TTL: 24 часа
- Источник в коде:
backend/app/api/v1/endpoints/reschedule.py::create_reschedule_proposal()
CR-005: Перенос записи через Telegram (provider)
- Описание: Провайдер переносит запись через inline-кнопки в Telegram
- Условие: Нажатие кнопки "Перенести" (callback_data: reschedule_{booking_id})
- Действие:
- Проверка: status in ["pending", "confirmed"] и нет PENDING proposal
- Загрузка расписания + существующих записей на дату
- Расчёт доступных часов (шаг 15 мин, проверка busy minutes)
- Показ inline-клавиатуры с доступными часами (rh_{booking_id}_{hour})
- После выбора часа — показ минут: :00, :15, :30, :45 (rm_{booking_id}{hour}{min})
- Создание RescheduleProposal (proposed_by: "PROVIDER_BOT")
- Уведомление клиенту
- Исключения:
- Перенос только на ТУ ЖЕ дату (через Telegram нельзя выбрать другую дату)
- Через API (CR-004) можно выбрать любую дату
- Если нет доступных слотов — "Нет доступного времени для переноса"
- Источник в коде:
backend/app/services/telegram_handler.py::_handle_reschedule_init()
CR-006: Принятие переноса клиентом
- Описание: Клиент принимает предложение переноса через Telegram
- Условие: Нажатие кнопки ra_{proposal_id} в Telegram
- Действие:
- Поиск proposal по ID
- Проверка: proposal.status == PENDING
- Обновление booking: start_at = proposed_date + proposed_time, end_at = start_at + duration
- proposal.status = ACCEPTED
- Уведомление провайдеру: "Клиент подтвердил перенос"
- Редактирование сообщения клиенту (убираются кнопки)
- Исключения:
- Нет повторной проверки доступности слота при принятии (возможен overlap)
- Если proposal expired — ответ: "Предложение истекло"
- Источник в коде:
backend/app/services/telegram_handler.py::_handle_reschedule_accept()
CR-007: Отклонение переноса клиентом
- Описание: Клиент отклоняет предложение переноса
- Условие: Нажатие кнопки rd_{proposal_id} в Telegram
- Действие:
- proposal.status = DECLINED
- Запись остаётся на прежнем времени (без изменений)
- Уведомление провайдеру: "Клиент отклонил перенос"
- Исключения:
- Провайдер может создать новый proposal после отклонения
- Источник в коде:
backend/app/services/telegram_handler.py::_handle_reschedule_decline()
CR-008: Автоистечение предложения переноса
- Описание: RescheduleProposal автоматически истекает через 24 часа
- Условие: proposal.expires_at < now AND status == PENDING
- Действие:
- Lazy expiration: статус меняется на EXPIRED при GET /reschedule/{booking_id}
- Нет фонового процесса (no cron, no celery)
- Запись остаётся на прежнем времени
- Исключения:
- Если никто не запрашивает GET — proposal "висит" в PENDING бесконечно
- Клиент может ответить на expired proposal до первого GET — поведение не определено
- Источник в коде:
backend/app/api/v1/endpoints/reschedule.py::get_reschedule_proposals()
CR-009: Ограничение на время отмены — НЕ реализовано
- Описание: Нет ограничения: "отменить можно только за N часов до записи"
- Условие: —
- Действие: TODO: не реализовано. Провайдер может отменить запись за 1 минуту до начала.
- Исключения: Клиент вообще не может отменить (см. CR-003)
- Источник в коде: Отсутствует
CR-010: No-show обработка — НЕ реализована
- Описание: Нет автоматической обработки no-show (клиент не пришёл)
- Условие: —
- Действие: TODO: не реализовано. Нет статуса "no_show", нет cron-job для auto-complete/cancel
- Исключения:
- Запись остаётся в "confirmed" навсегда, если провайдер не переведёт в "completed" или "cancelled" вручную
- Источник в коде: Отсутствует
CR-011: Возврат слота при отмене
- Описание: При отмене записи слот автоматически становится доступным
- Условие: booking.status переходит в "cancelled" или "rejected", ИЛИ booking удаляется
- Действие:
- AvailabilityService.get_bookings_for_date() фильтрует: status NOT IN ("cancelled", "rejected")
- Отменённая запись исчезает из busy_intervals
- Слот становится доступен для новых записей
- Исключения:
- Нет уведомления "слот освободился" другим ожидающим клиентам (waitlist не реализован)
- При hard delete (DELETE /bookings/{id}) — запись удалена, слот свободен автоматически
- Источник в коде:
backend/app/services/availability.py::get_bookings_for_date()(строка 72)