Туристическое агентство принимало заявки вручную: проверка номеров, письма, таблица, подтверждения. Я собрал пайплайн, который делает это сам — атомарно, без двойных броней и с онлайн-оплатой.
Семь нод превращают входящую заявку в подтверждённую оплату — без единого ручного действия.
Менеджер вручную проверял свободные номера, писал клиенту, обновлял таблицу и отправлял подтверждение. Заявки обрабатывались часами.
Из-за ручного обновления таблицы свободный номер могли забронировать дважды — конфликт, извинения, потеря клиента.
Неоплаченные брони висели вечно и блокировали номера для других гостей.
Всё развёрнуто на одном VPS через Dokploy (Docker). Секреты — в n8n Credentials и таблице AppConfig, ничего не хранится в коде.
| Компонент | Роль | Версия |
|---|---|---|
| n8n | Оркестрация: 4 workflow, webhook, schedule, Code-узлы | 2.31.5 self-hosted |
| Supabase | PostgreSQL: таблицы, триггеры, SQL-функции, RLS | Postgres 15 |
| Robokassa | Приём платежей: ссылка на оплату + Result-уведомления | sandbox → prod |
| Telegram Bot | Мгновенные уведомления менеджеру + отчёт в 18:00 | Bot API |
| SMTP | Письма клиенту: подтверждение / отказ / оплата | Mailtrap → боевой |
| Dokploy | Деплой и окружения: n8n, Supabase, NocoDB | Docker |
Основной пайплайн обработки заявки и три сервисных. Каждый защищён от сбоев: ноды
onError → continue, Global Error Handler, ретраи.
Принимает заявку, нормализует данные, атомарно создаёт бронь через book_room(),
генерирует ссылку Robokassa, отправляет письмо и уведомление. Корректные HTTP-коды: 200
/ 400 / 409 / 503.
IN_Webhook_Form → IN_Validate_Normalize → CORE_Book_Room_RPC → SWITCH_Booking_Result → CORE_Generate_Payment_Url → OUT_Email_Client → OUT_Notify_Manager
Принимает Result URL от Robokassa, проверяет MD5-подпись, подтверждает оплату и переводит бронь в
confirmed.
Каждый день в 18:00 (Europe/Moscow) отправляет менеджеру отчёт: новые брони, неподтверждённые, номера в reserved.
Каждые 15 минут отменяет неоплаченные брони старше 24 часов и освобождает номера. Менеджер получает список отменённых броней в Telegram.
6 таблиц, 8 SQL-функций, триггеры аудита и освобождения номеров. Ниже — живое превью данных.
public.Rooms| id | Отель | Тип | Даты | Цена | Статус |
|---|---|---|---|---|---|
| 3 | Test Hotel | Standard | 01.08–31.08 | 10 000 ₽ | reserved |
| 4 | Test Hotel | Standard | 01.08–31.08 | 10 000 ₽ | reserved |
| 6 | Test Hotel | Standard | 01.08–31.08 | 10 000 ₽ | free |
| 7 | Test Hotel | Standard | 01.08–31.08 | 10 000 ₽ | free |
public.Bookings| id | Клиент | Даты | Бронь | Оплата |
|---|---|---|---|---|
| 9 | Тест Форма | 21.08–26.08 | confirmed | paid |
| 11 | Тест Оплата | 23.08–28.08 | reserved | pending |
| 6 | Иван Тестов | 14.08–19.08 | cancelled | pending |
| 7 | Иван Тестов | 16.08–21.08 | cancelled | pending |
Главный риск задачи — race condition: два клиента одновременно бронируют один номер. Решение — многослойное.
[SQL]Функция book_room() одной транзакцией: блокирует номер FOR UPDATE,
проверяет status='free', создаёт броню. Любой сбой — полный откат.
[UUID]Форма генерирует idempotency_key. Повторная отправка той же заявки вернёт
существующую бронь, а не создаст дубль.
[UNIQUE]Уникальный индекс: один email + отель + тип + даты = одна активная броня. Даже с новым ключом дубль не пройдёт.
[try/catch]Все внешние вызовы обёрнуты, ошибки логируются в workflow_errors, менеджер получает
алерт. Ни одна заявка не теряется — невалидные уходят в карантин.
Клиент платит по ссылке из письма. Robokassa шлёт Result-уведомление, подпись проверяется, статус меняется сам.
status: reserved, payment: pending
Тема: Бронирование подтверждено — Test Hotel