Rental Booking (Car, Equipment) on Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Rental Booking (Car, Equipment) on Website
Complex
~2-4 weeks
FAQ

Our competencies:

Development stages

Latest works

  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1262
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    874
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    851

Implementing Rental Booking (Car, Equipment) on a Website

Rental differs from hotels in two ways: the rental unit moves physically (car leaves), and duration is measured in hours, not just days. This creates specific tasks: tracking pickup and return locations, hourly pricing, deposits.

Data Model

CREATE TABLE rental_items (
    id              SERIAL PRIMARY KEY,
    category_id     INTEGER,
    name            VARCHAR(255) NOT NULL,
    description     TEXT,
    vin_or_serial   VARCHAR(100),           -- VIN for cars, serial for equipment
    license_plate   VARCHAR(20),            -- cars only
    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 '{}',     -- specs (power, volume, 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),            -- for cars
    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,              -- actual return
    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 '[]',     -- additional services
    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'))
);

Price Calculation

Rental can be priced differently: first hours at one rate, then daily:

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']:
        # Hourly rental
        base_price = Decimal(str(item['hourly_rate'])) * Decimal(str(total_hours))
        billing_unit = 'hourly'
    else:
        # Daily (ceil — full day for partial day)
        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'])),
    }

Deposit (Security Deposit)

Deposit is frozen on card on booking confirmation via Payment Intent with 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',  # freeze only, don't charge
        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):
    # Partial or full deposit charge for damages
    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)

Late Return Tracking

Cron job every 30 minutes finds active rentals with expired return_at:

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)

Late fee = daily rate × number of late days (minimum 1).

Pickup and Return Locations

Rental at different locations (one-way) may cost more. One-way fee stored in routes table:

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)
);

Customer Documents

For car rentals, driver's license required. Document photos uploaded on booking and stored in protected S3 bucket with restricted access. Links valid 15 minutes (presigned URL).

Implementation Timeline

Basic rental of one item type without deposit and one-way — 8–11 business days. Daily + hourly pricing, Stripe deposit, late fees, multiple pickup points, document upload, customer portal — 14–20 business days.