Брони и slot-engine

Статусы броней, переходы, public-флоу с cancel-токеном, оплачиваемые брони, concurrency-safe createBooking.

Брони и slot-engine

Запись клиента на услугу — это booking. Брони идут через concurrency-safe createBooking: SELECT FOR UPDATE на пересекающихся бронях того же ресурса + re-check time-off внутри транзакции. Это защищает от ситуации «два клиента одновременно нажали "записаться" на один слот».

Partial-unique-index на (resourceId, startAt) не используется, потому что легальные cancelled/completed-брони перекрываются с новыми, и это сломало бы constraint. Защита — на app-уровне в транзакции.

Статусы брони

СтатусЧто значит
pending_paymentСоздана, ждёт оплату или подтверждения от бизнеса. Слот залочен.
confirmedПодтверждена (оплачена или бесплатная — принята).
cancelledОтменена клиентом или бизнесом. Слот освобождён.
completedУслуга оказана. Финальное состояние.
no_showКлиент не пришёл. Финальное состояние.

Переходы (в админке /account/bookings):

pending_payment ─→ confirmed ─┬─→ completed
                              └─→ no_show
                              └─→ cancelled
pending_payment ─→ cancelled

Бесплатные и платные брони

УслугаЧто происходит при бронировании
requiresPayment=false (бесплатная)Сразу создаётся бронь со статусом confirmed (если бизнес не настроил ручное подтверждение). Никакого мерчанта/чекаута.
requiresPayment=true, prepaymentPercent=100Создаётся commerce_order на полную сумму, бронь — pending_payment. Webhook оплаты → confirmed.
requiresPayment=true, prepaymentPercent<100Создаётся commerce_order на сумму предоплаты, остаток платится на месте. Бронь идёт через тот же чекаут и тот же merchant_account, что и заказы товаров.

Бронь и заказ связаны через booking.orderIdcommerce_order.id. Webhook commerce_order.paid триггерит booking.confirmed.

Public-флоу записи

Клиенту не нужен аккаунт Framix. Он:

  1. Открывает страницу услуги (/s/<slug>) или общается с агентом.
  2. Выбирает дату/время из slot-picker'а (AppBookingWidget / day-picker в чате / inline-keyboard в TG/VK).
  3. Заполняет имя + email и/или телефон.
  4. Если услуга платная — попадает на чекаут (AppCheckoutWidget), оплачивает.
  5. Получает email с подтверждением и cancel-токеном.

Cancel-токен

В email — ссылка вида /api/bookings/public/<cancel_token>. По этой ссылке клиент:

  • видит свою бронь без логина (GET /api/bookings/public/[token]);
  • может отменить (POST /api/bookings/public/cancel).

После отмены токен инвалидируется. Поздняя отмена (после cancellationPolicyHours) помечается [late_cancel] в cancelReason — Framix только фиксирует факт, политика возврата на стороне бизнеса.

Public-endpoints

EndpointНазначение
GET /api/bookings/public/available-slotsСвободные слоты по serviceId или (serviceSlug + projectId). Учитывает visibility. Max 14-дневный диапазон, до 30 слотов в ответе.
POST /api/bookings/public/bookСоздание брони, возвращает cancelToken и (для платных) ссылку на чекаут.
GET /api/bookings/public/[token]Просмотр своей брони по cancel-token.
POST /api/bookings/public/cancelОтмена по token.

Owner-управление

/account/bookings или /account/services → вкладка «Записи».

  • Список с фильтрами (статус, услуга, поиск по клиенту).
  • Группировка по дням.
  • Drawer-деталка с переходами статусов и контактами клиента.
  • Кнопка «Открыть карточку клиента» — переход в единый клиент workspace.

Cancel-токен не отображается в owner-UI — это секрет, выданный клиенту по email.

Уведомления

Триггеры dispatchNotification:

СобытиеКому
Новая броньвладельцу — in-app + email + (опционально) Telegram-DM / VK-DM
Бронь подтверждена / оплаченаклиенту — email
Отмена брони (клиент или бизнес)владельцу
Напоминание о завтрашней брониклиенту (cron, поле reminderSentAt защищает от дублей)

Настройки доставки — /account/notifications/settings.

Что хранится

ТаблицаЧто
bookingЗапись клиента: ресурс, услуга, время, контакты, статус, токен отмены

Связь с другими сущностями

  • booking.orderIdcommerce_order (для платных броней).
  • booking.collectionIdproject_collection (лид в CRM, автоматически).
  • booking.clientIdworkspace_client (единый клиент workspace, через upsert по email/phone).
  • booking.agentId / booking.sessionIdai_agents / ai_chat_session (если запись пришла из чата агента).

Виджет на сайте

Чтобы клиенты могли записываться прямо со страницы услуги:

  1. В редакторе сайта добавьте блок «Запись на услугу» из палитры (раздел «Услуги»).
  2. В свойствах блока выберите услугу из каталога.
  3. На опубликованной странице появится календарь с днями и slot-picker для выбранного дня.

Альтернативно — на странице услуги (/s/<slug>) есть встроенный slot-picker, и можно дать на неё прямую ссылку из лендинга.

Если slot-engine ничего не отдаёт

Проверьте по порядку:

  1. Услуга активна (isActive=true).
  2. Ресурс активен (если есть).
  3. Привязка услуга ↔ ресурс есть (в карточке услуги отмечен мастер).
  4. Есть availability_rule для этого ресурса (или для всех — если ресурса нет).
  5. Нет глобального time-off, накрывающего запрашиваемый диапазон.
  6. Lead-часы / horizon-дни не отрезают всё (если bookingLeadHours=2, слоты на «прямо сейчас» не покажутся).
  7. На вкладке «Расписание» в превью-сетке должны быть зелёные дни.

Если всё ОК и слотов всё равно нет — посмотрите Network в браузере, эндпоинт GET /api/bookings/public/available-slots вернёт код ошибки или пустой массив с диагностикой.

На этой странице