Skip to main content

Overview

The public booking flow is a 3-step funnel designed for non-technical users to quickly reserve a parking spot. It features real-time dynamic pricing, vehicle type detection, and optional additional services.
Location: /booking route
View: views/booking.ejs
Controller: controllers/mainController.js:renderBooking()
Client Logic: public/javascripts/public-booking.js

Workflow: 3-Step Funnel

Step 1: Dates & Service Selection

Customers start by selecting their parking dates and desired service tier.
1

Select Entry/Exit Dates

  • Entry Date: datetime-local input (e.g., “2024-03-15 09:00”)
  • Exit Date: datetime-local input (e.g., “2024-03-20 18:00”)
  • Dates can be pre-filled via URL params: /booking?entrada=2024-03-15T09:00&salida=2024-03-20T18:00
2

Choose Main Service

Options fetched from service_catalog_dao.getMainServices(true) (only active services):
  • Basic Parking: Outdoor, self-park
  • Premium Parking: Covered, valet service
  • VIP Experience: Climate-controlled, car wash included
3

Add Optional Extras

Checkbox grid for additional services (from additional_service table):
  • Car wash (+€15)
  • Tire pressure check (+€5)
  • Fuel refill (+€10)
  • Interior cleaning (+€20)
4

Real-Time Price Calculation

Price updates automatically on any change via updateDynamicPrice():
POST /api/pricing/dynamic
{
  "entry_date": "2024-03-15T09:00",
  "exit_date": "2024-03-20T18:00",
  "vehicle_type": "TURISMO",
  "id_main_service": 1,
  "additional_services": [2, 5]
}
Response includes total price calculated by pricingService.calculateTotalPrice()
Validation:
  • Entry date < Exit date (enforced client-side and server-side)
  • Service selection is required
  • If no service selected, uses cheapest service for estimation

Step 2: Vehicle & Customer Details

Collect customer information and vehicle details for reservation.
  • Full Name: Required (pre-filled if logged in: user.nombre)
  • Phone: Required (pre-filled: user.numero)
  • Email: Optional (pre-filled: user.email)
Data stored in customer table. Phone used as unique identifier for walk-in customers.
  • License Plate: Required, auto-uppercase (e.g., “0000XXX”)
  • Vehicle Type: Dropdown (TURISMO, FURGONETA, MOTOCICLETA, etc.)
  • Brand: Optional (e.g., “Toyota”)
  • Model: Optional (e.g., “Corolla”)
  • Color: Optional
If user is logged in and has vehicles, first vehicle auto-fills from customer_dao.getVehiclesByCustomerId()
Auto-fill Logic (authController.js:90-110):
if (req.session.user && req.session.user.id) {
    const vehicles = await customerDAO.getVehiclesByCustomerId(req.session.user.id);
    if (vehicles && vehicles.length > 0) {
        userVehicle = vehicles[0]; // First registered vehicle
    }
}

Step 3: Confirmation & Submission

Final review before creating the reservation.

Summary Display

Shows all collected data:
  • Entry/Exit dates (formatted)
  • Customer name & phone
  • License plate (uppercase)
  • Vehicle description
  • Final calculated price

Submission Handler

POST /api/reservations/publicCreates reservation via reservationDAO.createReservationTransaction():
  1. Find or create customer by phone
  2. Find or create vehicle by license plate
  3. Link customer to vehicle
  4. Create reservation with status PENDIENTE
  5. Insert additional services
Success Response:
{
  "success": true,
  "message": "Booking confirmed",
  "data": {
    "id_reservation": 42,
    "customer_name": "John Doe",
    "license_plate": "1234ABC",
    "entry_date": "15/3/2024, 9:00:00",
    "total_price": 125.50
  }
}
A success screen shows the booking ID and allows return to home.

Price Calculation Engine

Dynamic Pricing Algorithm

Implemented in services/pricingService.js:82-139:
1

Calculate Stay Duration

const diffTime = Math.abs(exitDate - entryDate);
const calculatedDays = Math.floor(diffTime / msPerDay);
const courtesyTime = 2 * 60 * 60 * 1000; // 2-hour grace period

if (diffTime % msPerDay > courtesyTime) {
    calculatedDays += 1;
}
const totalDays = calculatedDays > 0 ? calculatedDays : 1; // Min 1 day
2

Lookup Base Rate

Find matching rate from service_rate table:
SELECT daily_price FROM service_rate
WHERE id_main_service = ? 
  AND min_days <= ? 
  AND max_days >= ?
Example rates:
  • 1-3 days: €15/day
  • 4-7 days: €12/day
  • 8-15 days: €10/day
3

Apply Vehicle Multiplier

From vehicle_type_coefficient table:
  • TURISMO: 1.0x
  • FURGONETA: 1.5x
  • MOTOCICLETA: 0.8x
  • CAMION: 2.0x
4

Add Extras

Sum fixed prices from additional_service table
5

Calculate Final Price

const totalPrice = (baseRate.daily_price * totalDays * vehicleCoefficient) + extrasTotal;
return Math.round(totalPrice * 100) / 100; // 2 decimals

Pricing Cache

All pricing data is cached in RAM on server startup via PricingService.initCache() to avoid repeated database queries. Cache includes:
  • Vehicle type coefficients (Map)
  • Service rates (Array)
  • Additional service prices (Map)

User Experience Features

URL Pre-population

Users can arrive from / with dates already selected:
/booking?entrada=2024-03-15T09:00&salida=2024-03-20T18:00
Dates auto-fill in Step 1

Logged-in User Benefits

  • Auto-fill name, email, phone
  • First registered vehicle pre-selected
  • Faster checkout experience

Visual Validation

Red borders (.is-invalid) on empty required fields when advancing steps

Loading States

Spinner displays during price calculation API calls

Technical Implementation

Frontend Flow Control

Step Navigation (public-booking.js:126-176):
function goToStep(step) {
    let errores = false;
    
    if (step === 2) {
        // Validate dates and service
        if (!entry.value) { mostrarError(entry); errores = true; }
        if (!exit.value) { mostrarError(exit); errores = true; }
        if (!service.value) { mostrarError(service); errores = true; }
    }
    
    if (step === 3) {
        // Validate customer & vehicle
        if (!name.value) { mostrarError(name); errores = true; }
        if (!phone.value) { mostrarError(phone); errores = true; }
        if (!plate.value) { mostrarError(plate); errores = true; }
    }
    
    if (errores) return;
    
    // Show/hide step panels
    document.querySelectorAll('.booking-body').forEach(b => b.style.display = 'none');
    document.querySelector(`#booking-step-${step}`).style.display = 'block';
}

Backend Transaction Safety

Atomic Reservation Creation (models/reservation-dao.js:239-354):
async createReservationTransaction(customerData, vehicleData, reservationData) {
    const client = await db.connect();
    
    try {
        await client.query('BEGIN');
        
        // 1. Find or create customer by phone
        let customerId = await findOrCreateCustomer(customerData);
        
        // 2. Find or create vehicle by license plate
        let vehicleId = await findOrCreateVehicle(vehicleData);
        
        // 3. Link customer <-> vehicle (if not exists)
        await linkCustomerVehicle(customerId, vehicleId);
        
        // 4. Create reservation (status = PENDIENTE)
        const reservationId = await insertReservation(...);
        
        // 5. Insert additional services
        await insertAdditionalServices(reservationId, additionalServices);
        
        await client.query('COMMIT');
        return reservationId;
        
    } catch (error) {
        await client.query('ROLLBACK');
        throw error;
    } finally {
        client.release();
    }
}
ROLLBACK on failure: If any step fails (e.g., duplicate vehicle constraint), the entire transaction is rolled back—no partial data is saved.

Code References

ComponentFile PathKey Functions
Main Controllercontrollers/mainController.jsrenderBooking() (line 89-127)
Public APIroutes/api.js/api/reservations/public endpoint
Reservation DAOmodels/reservation-dao.jscreateReservationTransaction() (line 239-354)
Pricing Serviceservices/pricingService.jscalculateTotalPrice() (line 82-139)
Client Logicpublic/javascripts/public-booking.jsgoToStep(), updateDynamicPrice()
View Templateviews/booking.ejs3-step form structure

Error Handling

// Date logic check
if (new Date(entry.value) >= new Date(exit.value)) {
    Swal.fire({
        icon: 'error',
        title: 'Invalid Dates',
        text: 'Exit date must be after entry date.'
    });
    return;
}

Next Steps

User Profile

Manage reservations after booking

Admin Dashboard

How staff processes bookings

Build docs developers (and LLMs) love