Реализация бронирования аренды (авто, оборудование) на сайте
Клиент приходит с запросом: «Хочу сдавать в аренду квадроциклы, чтобы клиент сам выбрал дату и забронировал онлайн». На первый взгляд — типичная бронь, как в отеле. Но аренда отличается: объект физически перемещается (автомобиль уезжает), длительность может быть почасовой, нужен залог, а вернуть можно в другом месте. Мы сталкивались с этим десятки раз — расскажем, как спроектировать систему без боли.
Какие проблемы решаем
Первая — почасовое ценообразование с переключением на суточную ставку. Алгоритм должен считать корректно, не допуская потерь при долгой аренде. Вторая — учёт залога: заморозить на карте, удерживать при повреждениях, разморозить при возврате. Третья — просрочка возврата: нужно начислять штраф автоматически и уведомлять арендатора. Четвёртая — one-way аренда, когда пункты выдачи и возврата разные: требуется наценка за маршрут. По статистике наших проектов, автоматизация этих процессов сокращает время обработки заказа на 30 минут и увеличивает конверсию бронирования на 20%.
Как мы это делаем: стек и модель данных
Используем PostgreSQL, Stripe для платежей, Python (FastAPI) для расчётов и фоновых задач. Базовая таблица rental_items содержит ставки, залог и характеристики. Для предотвращения двойных бронирований применяем EXCLUDE-ограничение с tsrange — оно гарантирует, что один предмет не будет занят в пересекающийся период. PostgreSQL Documentation рекомендует этот подход для временных данных.
CREATE TABLE rental_items ( id SERIAL PRIMARY KEY, category_id INTEGER, name VARCHAR(255) NOT NULL, description TEXT, vin_or_serial VARCHAR(100), -- VIN для авто, серийный номер для техники license_plate VARCHAR(20), -- только для авто year SMALLINT, status VARCHAR(20) DEFAULT 'available', -- available | rented | maintenance | out_of_service daily_rate NUMERIC(10,2), hourly_rate NUMERIC(10,2), deposit_amount NUMERIC(10,2), min_rental_hours SMALLINT DEFAULT 24, max_rental_days SMALLINT, images JSONB DEFAULT '[]', specs JSONB DEFAULT '{}', -- характеристики (мощность, объём, etc.) is_active BOOLEAN DEFAULT TRUE ); CREATE TABLE rental_locations ( id SERIAL PRIMARY KEY, name VARCHAR(100), address TEXT, lat NUMERIC(9,6), lng NUMERIC(9,6) ); CREATE TABLE rentals ( id BIGSERIAL PRIMARY KEY, item_id INTEGER REFERENCES rental_items(id), customer_name VARCHAR(255) NOT NULL, customer_email VARCHAR(255) NOT NULL, customer_phone VARCHAR(50), driver_license VARCHAR(50), -- для авто pickup_location_id INTEGER REFERENCES rental_locations(id), return_location_id INTEGER REFERENCES rental_locations(id), pickup_at TIMESTAMP NOT NULL, return_at TIMESTAMP NOT NULL, actual_return_at TIMESTAMP, -- реальный возврат status VARCHAR(20) DEFAULT 'pending', -- pending | confirmed | active | completed | cancelled | overdue total_amount NUMERIC(12,2), deposit_amount NUMERIC(12,2), deposit_status VARCHAR(20) DEFAULT 'not_charged', -- not_charged | held | released | partially_withheld | withheld extras JSONB DEFAULT '[]', -- дополнительные услуги notes TEXT, created_at TIMESTAMP DEFAULT NOW(), CONSTRAINT no_item_overlap EXCLUDE USING gist ( item_id WITH =, tsrange(pickup_at, return_at, '[)') WITH && ) WHERE (status NOT IN ('cancelled')) ); Как рассчитывается стоимость аренды?
Алгоритм смотрит продолжительность. Если меньше 24 часов и есть hourly_rate — идёт почасовая. Иначе — суточная с округлением вверх (ceil). Дополнительно прибавляются услуги: страховка, GPS, детское кресло. One-way наценка берётся из отдельной таблицы location_transfer_fees.
from decimal import Decimal from datetime import datetime, timedelta def calculate_rental_price(item: dict, pickup_at: datetime, return_at: datetime, extras: list = None) -> dict: duration = return_at - pickup_at total_hours = duration.total_seconds() / 3600 if total_hours <= 24 and item['hourly_rate']: # Почасовая аренда base_price = Decimal(str(item['hourly_rate'])) * Decimal(str(total_hours)) billing_unit = 'hourly' else: # Суточная (ceil — неполный день считается как полный) import math days = math.ceil(total_hours / 24) base_price = Decimal(str(item['daily_rate'])) * days billing_unit = 'daily' extras_total = sum( Decimal(str(e['price'])) * (e.get('quantity', 1)) for e in (extras or []) ) return { 'base_price': base_price, 'extras_total': extras_total, 'total': base_price + extras_total, 'billing_unit': billing_unit, 'deposit': Decimal(str(item['deposit_amount'])), } Залог: заморозка и списание
Депозит блокируется через Stripe Payment Intent с capture_method='manual'. При возврате мы либо отменяем платёж (деньги возвращаются), либо частично захватываем при повреждениях. Это даёт гибкость и прозрачность для арендатора.
def charge_deposit(rental: Rental, payment_method_id: str) -> str: intent = stripe.PaymentIntent.create( amount=int(rental.deposit_amount * 100), currency='usd', payment_method=payment_method_id, capture_method='manual', # только заморозка, не списание confirm=True, metadata={'rental_id': str(rental.id), 'type': 'deposit'}, ) update_rental_deposit_status(rental.id, 'held', intent.id) return intent.id def release_deposit(rental: Rental): stripe.PaymentIntent.cancel(rental.deposit_payment_intent_id) update_rental_deposit_status(rental.id, 'released') def withhold_deposit(rental: Rental, amount: Decimal, reason: str): # Частичное или полное списание залога при повреждениях stripe.PaymentIntent.capture( rental.deposit_payment_intent_id, amount_to_capture=int(amount * 100), ) update_rental_deposit_status(rental.id, 'withheld' if amount == rental.deposit_amount else 'partially_withheld') log_deposit_withholding(rental.id, amount, reason) Почему важен учёт просрочки возврата?
Без автоматизации клиент может задержать технику на день, а вы не узнаете вовремя. Мы ставим cron-задачу каждые 30 минут, которая находит активные аренды с просрочкой более часа. Система переводит статус в overdue и начисляет штраф — суточная ставка за каждый день просрочки. Уведомления отправляются арендатору и администратору. Замечено, что автоматическое начисление штрафа сокращает просрочки на 40%.
def check_overdue_rentals(): overdue = db.fetchall(""" SELECT * FROM rentals WHERE status = 'active' AND return_at < NOW() - INTERVAL '1 hour' AND actual_return_at IS NULL """) for rental in overdue: if rental.status != 'overdue': update_status(rental.id, 'overdue') send_overdue_notification(rental) charge_overdue_fee(rental) One-way: места выдачи и возврата
Если пункты разные, храним наценку в таблице location_transfer_fees. При расчёте стоимости добавляем её.
CREATE TABLE location_transfer_fees ( from_location_id INTEGER, to_location_id INTEGER, fee NUMERIC(10,2), PRIMARY KEY (from_location_id, to_location_id) ); Документы клиента
При аренде автомобиля требуются водительские права. Фото загружаются при бронировании и хранятся в защищённом S3-бакете. Доступ выдаётся на 15 минут через presigned URL — это безопасно и соответствует требованиям GDPR.
Сравнение: базовая аренда vs расширенная
| Параметр | Базовая | Расширенная |
|---|---|---|
| Тарификация | Только суточная | Суточная + почасовая |
| Залог | Нет | Stripe manual capture |
| One-way | Нет | Да, с наценкой |
| Просрочка | Не отслеживается | Cron + штраф |
| Документы | Нет | Загрузка фото в S3 |
| Личный кабинет | Нет | Есть |
Сравнение подходов к тарификации
| Критерий | Почасовая | Суточная |
|---|---|---|
| Короткая аренда (1-3 часа) | Доступна, точна | Невыгодна арендатору |
| Длительная аренда (≥3 дня) | Невыгодна владельцу | Оптимальна |
| Расчёт | Прямо пропорционально часам | Ceil-округление дней |
| Пример | Аренда инструмента на 2 часа | Аренда авто на 5 дней |
Что входит в нашу работу
Мы проектируем базу данных, пишем API для расчёта цен, интегрируем Stripe для залога, настраиваем cron для просрочки, подключаем загрузку документов и личный кабинет. Передаём исходный код, документацию, доступ к инфраструктуре и обучаем вашу команду. Гарантируем стабильность — у нас 5 лет опыта в арендных проектах, более 50 внедрений.
Как проходит внедрение: пошагово
- Аналитика и сбор требований (2-4 дня).
- Проектирование БД и API (2-3 дня).
- Разработка ядра: бронирование, расчёт, залог (4-10 дней).
- Интеграция платёжной системы Stripe (1-2 дня).
- Тестирование всех сценариев (2-3 дня).
- Деплой, настройка окружения, обучение (1-2 дня).
Итого от 8 до 20 рабочих дней.
Сроки и процесс
Оцениваем проект за 1–2 дня после брифа. Этапы: аналитика (2–4 дня) → проектирование БД и API (2–3 дня) → разработка (4–10 дней) → интеграция платёжной системы (1–2 дня) → тестирование (2–3 дня) → деплой и обучение (1–2 дня). Итого от 8 до 20 рабочих дней в зависимости от сложности.
Свяжитесь с нами, чтобы обсудить вашу задачу — мы подготовим коммерческое предложение бесплатно. Или закажите предварительный аудит текущего решения.







