Реалізація бронювання столиків ресторану на веб-сайту
Бронювання столика — завдання з кількома нетривіальними деталями: вміст столика повинна відповідати кількості гостей, ресторан працює по змінах (обід/ужин), а перепродаж — іноді бажана поведінка.
Модель зали та столів
CREATE TABLE restaurant_halls (
id SERIAL PRIMARY KEY,
name VARCHAR(100), -- 'Основний зал', 'Тераса', 'VIP'
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE restaurant_tables (
id SERIAL PRIMARY KEY,
hall_id INTEGER REFERENCES restaurant_halls(id),
table_number VARCHAR(10),
min_guests SMALLINT DEFAULT 1,
max_guests SMALLINT NOT NULL,
is_combinable BOOLEAN DEFAULT FALSE, -- можна ли сдвигати зі сміжними
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE table_bookings (
id BIGSERIAL PRIMARY KEY,
table_id INTEGER REFERENCES restaurant_tables(id),
guest_name VARCHAR(255) NOT NULL,
guest_phone VARCHAR(50) NOT NULL,
guest_email VARCHAR(255),
guests_count SMALLINT NOT NULL,
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
special_wishes TEXT,
deposit_paid BOOLEAN DEFAULT FALSE,
deposit_amount NUMERIC(10,2),
source VARCHAR(30) DEFAULT 'website', -- 'website','phone','walkin'
created_at TIMESTAMP DEFAULT NOW()
);
Зміни і часові слоти
Ресторан не броніює стіл «на 15:37» — лише на змін або фіксований слот:
SHIFTS = {
'lunch': {'start': time(12, 0), 'end': time(15, 0), 'duration': timedelta(hours=2)},
'dinner': {'start': time(18, 0), 'end': time(23, 0), 'duration': timedelta(hours=2)},
}
def get_booking_slots(date: date) -> list[dict]:
slots = []
for shift_name, shift in SHIFTS.items():
current = datetime.combine(date, shift['start'])
end_dt = datetime.combine(date, shift['end'])
while current + shift['duration'] <= end_dt:
slots.append({
'shift': shift_name,
'starts_at': current,
'ends_at': current + shift['duration'],
})
current += timedelta(hours=1) # слоти щогодини
return slots
Підбір столика за кількістю гостей
def find_available_table(guests: int, starts_at: datetime, ends_at: datetime, hall_id: int = None):
query = """
SELECT t.*
FROM restaurant_tables t
WHERE t.min_guests <= %(guests)s
AND t.max_guests >= %(guests)s
AND t.is_active = TRUE
AND (%(hall_id)s IS NULL OR t.hall_id = %(hall_id)s)
AND t.id NOT IN (
SELECT table_id FROM table_bookings
WHERE status NOT IN ('cancelled', 'no_show')
AND tsrange(starts_at, ends_at, '[)') && tsrange(%(starts)s, %(ends)s, '[)')
)
ORDER BY t.max_guests ASC -- вибрати найменший підходящий стіл
LIMIT 1
"""
return db.fetchone(query, {
'guests': guests, 'hall_id': hall_id,
'starts': starts_at, 'ends': ends_at,
})
Об'єднання столів
Якщо немає столика на 8 осіб, але есть два сміжних столики по 4 — можна запропонувати об'єднання. Потребує таблицю сміжності:
CREATE TABLE table_adjacency (
table_a INTEGER REFERENCES restaurant_tables(id),
table_b INTEGER REFERENCES restaurant_tables(id),
PRIMARY KEY (table_a, table_b)
);
Підтвердження та нагадування
| Час | Дія |
|---|---|
| Відразу | SMS/email клієнту з деталями брони |
| За 24 години | SMS-нагадування |
| За 2 години | SMS «Чекаємо вас сьогодні о 19:00» |
| +30 хв після початку | Якщо не прийшли — зміна статусу на no_show, звільнення столика |
| +1 день | Email з проханням про відгук |
Опціональний депозит при бронюванні — інтеграція з платіжним шлюзом. Депозит списується, якщо гість не прийшов і не скасував за N годин.
Строки реалізації
Базова система з однією залою, змінами і SMS/email сповіщеннями — 6–8 робочих днів. Декілька зал, об'єднання столів, депозит, управління через CMS, історія брони — 10–13 робочих днів.







