Перейти к содержанию

Booking Rules — Правила бронирования

Извлечено из кода: backend/app/api/v1/endpoints/public.py, backend/app/models/booking.py, backend/app/services/availability.py, backend/app/schemas/public.py


BR-001: Создание записи через публичный API

  • Описание: Клиент создаёт бронирование через POST /public/{slug}/booking без авторизации
  • Условие: Клиент отправляет: service_id, date, time, first_name, phone. Опционально: master_id, notes, recaptcha_token
  • Действие:
  • Валидация reCAPTCHA (если token передан и RECAPTCHA_SECRET_KEY настроен)
  • Поиск бизнеса по slug — 404 если не найден
  • Поиск услуги по service_id — 404 если не найдена
  • Resolve master_id (см. BR-002)
  • Поиск/создание Client по phone + business_id (см. BR-003)
  • Расчёт end_at = start_at + service.duration минут
  • Нормализация телефона: убрать +, -, пробелы; если 10 цифр — добавить 7
  • Создание Booking со статусом "pending", price_snapshot = service.price
  • Отправка Telegram-уведомления провайдеру (inline-кнопки: подтвердить/отклонить/перенести)
  • Возврат BookingResponse с telegram_url для подписки клиента
  • Исключения:
  • reCAPTCHA не настроена (RECAPTCHA_SECRET_KEY пустой) — проверка пропускается
  • master_id=null — автоназначение (BR-002)
  • Источник в коде: backend/app/api/v1/endpoints/public.py::create_booking()

BR-002: Автоназначение мастера

  • Описание: Если клиент не выбрал мастера, система назначает автоматически
  • Условие: master_id = null в запросе на бронирование
  • Действие:
  • Поиск мастера через M2M таблицу master_services, привязанного к выбранной услуге (LIMIT 1)
  • Если не найден — поиск любого мастера бизнеса (LIMIT 1)
  • Если мастеров нет вообще — HTTP 400 "No masters available"
  • Исключения:
  • Алгоритм берёт первого найденного (по порядку в БД), без учёта загруженности
  • Все записи могут попасть к одному мастеру
  • Источник в коде: backend/app/api/v1/endpoints/public.py::create_booking() (строки 206-229)

BR-003: Создание/обновление клиента при записи

  • Описание: При каждой записи система ищет или создаёт клиента
  • Условие: Каждый POST booking
  • Действие:
  • SELECT Client WHERE phone = booking.phone AND business_id = business.id
  • Если не найден — INSERT новый Client (phone, first_name, notes, business_id)
  • Если найден — обновить first_name (если передан), дописать notes через перенос строки
  • Исключения:
  • Client привязан к конкретному бизнесу (один телефон = разные Client в разных бизнесах)
  • Notes накапливаются (append, не replace)
  • Источник в коде: backend/app/api/v1/endpoints/public.py::create_booking() (строки 231-255)

BR-004: Жизненный цикл статусов бронирования

  • Описание: Booking имеет жизненный цикл статусов
  • Условие: Любое изменение статуса
  • Действие:
  • pending -> confirmed (провайдер подтвердил)
  • pending -> cancelled (провайдер отклонил через reject)
  • pending -> rejected (провайдер отклонил — зарезервировано)
  • confirmed -> completed (запись завершена)
  • confirmed -> cancelled (провайдер отменил)
  • Исключения:
  • В коде НЕТ валидации переходов — PATCH /bookings/{id} принимает любой status без проверки текущего
  • Telegram callback confirm/reject работает ТОЛЬКО из pending (проверка: booking.status not in ["pending"])
  • Клиент НЕ может менять статус (нет endpoint)
  • Источник в коде:
  • Модель: backend/app/models/booking.py::BookingStatus
  • API: backend/app/api/v1/endpoints/bookings.py::update_booking_status()
  • Telegram: backend/app/services/telegram_handler.py::_handle_confirm_reject()

BR-005: Фиксация цены при бронировании (Price Snapshot)

  • Описание: Цена услуги копируется в booking при создании и не меняется
  • Условие: POST booking
  • Действие: booking.price_snapshot = service.price — значение на момент создания
  • Исключения:
  • Если Service.price изменится после создания booking — price_snapshot остаётся прежним
  • Аналитика (ClickHouse) использует price_snapshot, не текущую цену услуги
  • Источник в коде: backend/app/api/v1/endpoints/public.py::create_booking() (строка 277)

BR-006: Расчёт времени окончания записи

  • Описание: end_at вычисляется автоматически из start_at + duration
  • Условие: POST booking
  • Действие:
  • start_dt = datetime.combine(date, time)
  • end_dt = start_dt + timedelta(minutes=service.duration)
  • Исключения:
  • Время хранится как naive datetime (без timezone)
  • Нет проверки, что end_at не выходит за рабочие часы
  • Нет проверки overlap с другими записями (TODO в коде)
  • Источник в коде: backend/app/api/v1/endpoints/public.py::create_booking() (строки 258-259)

BR-007: Проверка доступности слота — НЕ реализована при записи

  • Описание: При создании записи бэкенд НЕ проверяет, свободен ли слот
  • Условие: POST booking
  • Действие: Запись создаётся без проверки overlap. Комментарий в коде: "TODO: Check overlap again to be safe?"
  • Исключения:
  • Race condition: два клиента одновременно бронируют один слот — оба успешно
  • Фронтенд проверяет слоты через GET /slots, но это не гарантирует атомарность
  • Источник в коде: backend/app/api/v1/endpoints/public.py::create_booking() (строки 261-262)

BR-008: Подтверждение/отклонение через Telegram

  • Описание: Провайдер подтверждает или отклоняет запись через inline-кнопки в Telegram
  • Условие: Провайдер нажимает кнопку подтверждения или отклонения на сообщении о новой записи
  • Действие:
  • Проверка: booking существует
  • Проверка: business.telegram_chat_id == текущий chat_id (авторизация)
  • Проверка: booking.status == "pending" (нельзя повторно обработать)
  • Обновление статуса: confirm -> "confirmed", reject -> "cancelled"
  • Редактирование исходного сообщения (убираются кнопки)
  • Уведомление клиента о смене статуса
  • Исключения:
  • reject ставит статус "cancelled", а не "rejected"
  • Если бот-сообщение удалено — edit_message упадёт, но ошибка ловится
  • Источник в коде: backend/app/services/telegram_handler.py::_handle_confirm_reject()

BR-009: Удаление записи (hard delete)

  • Описание: Провайдер может удалить запись через админку
  • Условие: DELETE /bookings/{id}
  • Действие:
  • Поиск booking по id + business_id (авторизация)
  • Если статус не "cancelled" — ставит "cancelled" и отправляет уведомление клиенту
  • Удаляет запись из БД (hard delete)
  • Исключения:
  • Hard delete — запись пропадает из БД навсегда (нет soft delete)
  • Клиент получает уведомление "Запись ОТМЕНЕНА" перед удалением
  • Источник в коде: backend/app/api/v1/endpoints/bookings.py::delete_booking()

BR-010: Нормализация телефона

  • Описание: Телефон клиента нормализуется при создании записи
  • Условие: POST booking
  • Действие:
  • Убираются символы: +, -, пробелы
  • Если осталось 10 цифр — добавляется "7" в начало (казахстанский формат)
  • Исключения:
  • Формат результата: 11 цифр без + (например, 77017017001)
  • При поиске подписок используются варианты: с +, без +, с 8 вместо 7
  • Источник в коде: backend/app/api/v1/endpoints/public.py::create_booking() (строки 265-267)

BR-011: Лимит записей в день — НЕ реализован

  • Описание: Нет ограничения на количество записей одного клиента в день
  • Условие:
  • Действие: TODO: не реализовано
  • Исключения: Клиент может создать неограниченное количество записей на одну дату
  • Источник в коде: Отсутствует

BR-012: Telegram URL в ответе на бронирование

  • Описание: После создания записи клиенту возвращается ссылка для подписки в Telegram
  • Условие: Каждый успешный POST booking
  • Действие: Генерируется URL: https://t.me/citakz_bot?start=client_{booking_id}
  • Исключения:
  • URL всегда возвращается, даже если у бизнеса нет Telegram-подписки
  • Клиент может проигнорировать ссылку — тогда уведомления не придут
  • Источник в коде: backend/app/api/v1/endpoints/public.py::create_booking() (строка 302)