# AIO Car Wash Booking System — Full Development Plan

> **Stack:** Laravel 13 · Inertia.js v2 · React 19 · TypeScript · Tailwind CSS 4 · Vite  
> **Goal:** A standalone, self-owned car wash booking platform replacing LatePoint. Location-first, vehicle-aware, mobile-service ready. Built to scale and extend with plugins.

---

## 1. Project Overview

A full-stack booking platform purpose-built for a car wash / mobile detailing business. Customers book online by first selecting their service location (mobile address or fixed wash location), then their vehicle, then their wash package. The system manages technician scheduling, real-time job tracking, payments, memberships, and fleet accounts — all from a single codebase the business fully owns and controls.

### Core Principles
- **Location-first:** Every booking starts with WHERE (address for mobile or branch for fixed)
- **Vehicle-aware:** Vehicle size (Sedan / SUV / Truck / Van) automatically adjusts pricing
- **Own your data:** No third-party plugin dependency, no WordPress lock-in
- **Built to extend:** Plugin/module system via Laravel Service Providers from day one
- **Mobile-ready:** Technician PWA for status updates and photo capture in the field

---

## 2. Tech Stack

### Frontend
| Tool | Purpose |
|------|---------|
| React 19 | UI framework |
| Inertia.js v2 | Server-driven SPA (no separate API needed) |
| TypeScript | Type safety across all components |
| Tailwind CSS 4 | Utility-first styling |
| shadcn/ui | Component library (built on Radix UI) |
| FullCalendar (React) | Visual booking calendar for admin |
| Zustand | UI state management |
| React Hook Form + Zod | Form handling and validation |
| Vite | Build tool + HMR |
| vite-plugin-pwa | Technician Progressive Web App |

### Backend
| Tool | Purpose |
|------|---------|
| Laravel 13 | Application framework |
| Inertia.js (Laravel adapter) | Connects controllers to React pages |
| PHP 8.3+ | Runtime |
| Laravel Sanctum | Session-based auth (works seamlessly with Inertia) |
| Spatie Laravel Permission | Role-based access control (Admin, Manager, Technician, Customer) |
| Laravel Cashier (Stripe) | Payments, subscriptions, invoices |
| Laravel Reverb | First-party WebSocket server (real-time job tracking) |
| Laravel Echo | Frontend WebSocket client |
| Laravel Horizon | Redis queue dashboard |
| Laravel Telescope | Local debugging |
| Laravel Scout + Meilisearch | Customer/booking search |
| Spatie Media Library | Before/after photo management |
| Spatie Laravel Permission | RBAC |
| nwidart/laravel-modules | Modular plugin structure |

### Database & Cache
| Tool | Purpose |
|------|---------|
| PostgreSQL 16 | Primary database |
| Redis | Cache, sessions, queues, pub/sub |
| PgBouncer | Connection pooling (production) |

### External APIs
| Service | Purpose |
|---------|---------|
| Stripe | Payments, subscriptions, Connect payouts |
| Twilio | SMS reminders, job status updates, 2-way messaging |
| Resend | Transactional email (confirmations, invoices) |
| Google Maps Platform | Address autocomplete, service zone validation |
| OpenWeatherMap | Rain forecast → auto-reschedule for mobile jobs |

### Infrastructure
| Tool | Purpose |
|------|---------|
| Laravel Forge or Ploi | Server provisioning |
| Laravel Octane (FrankenPHP) | High-performance PHP serving |
| Docker | Local dev environment parity |
| GitHub Actions | CI/CD pipeline |
| Cloudflare | CDN, DDoS protection |
| AWS S3 or Cloudflare R2 | Photo and file storage |

---

## 3. Database Schema

### Core Tables

```sql
-- Locations (fixed wash sites OR mobile zone definitions)
locations
  id, name, type (enum: fixed|mobile_zone),
  address, city, state, zip, lat, lng,
  service_radius_miles, service_zip_codes (jsonb),
  is_active, opens_at, closes_at,
  created_at, updated_at

-- Users (customers, staff, admins)
users
  id, name, email, phone, password,
  role (enum: admin|manager|technician|customer),
  stripe_customer_id, stripe_pm_id,
  created_at, updated_at

-- Vehicle profiles (belongs to user)
vehicles
  id, user_id,
  make, model, year, color,
  size (enum: sedan|suv|truck|van|oversized),
  license_plate, notes,
  is_default,
  created_at, updated_at

-- Wash packages
packages
  id, name, description, duration_minutes,
  price_sedan, price_suv, price_truck, price_van, price_oversized,
  is_active, display_order,
  created_at, updated_at

-- Add-on services (upsells)
addons
  id, name, description, duration_minutes,
  price_sedan, price_suv, price_truck, price_van, price_oversized,
  is_active, display_order,
  created_at, updated_at

-- Technicians (extends users)
technicians
  id, user_id,
  assigned_location_id,
  bio, profile_photo,
  travel_buffer_minutes (default: 20),
  is_available,
  created_at, updated_at

-- Bookings
bookings
  id, customer_id (users), technician_id,
  location_id, vehicle_id, package_id,
  service_address, service_city, service_state, service_zip, service_lat, service_lng,
  service_type (enum: mobile|fixed),
  scheduled_at, ends_at,
  status (enum: pending|confirmed|in_progress|completed|cancelled|no_show),
  subtotal, addons_total, total, deposit_amount, amount_paid,
  stripe_payment_intent_id, stripe_invoice_id,
  notes, internal_notes,
  cancellation_reason, cancelled_at,
  created_at, updated_at

-- Booking add-ons (pivot)
booking_addons
  id, booking_id, addon_id, price,
  created_at, updated_at

-- Job status log (finite state machine)
job_status_logs
  id, booking_id, status, note, lat, lng,
  created_by (user_id),
  created_at

-- Before/after photos (via Spatie Media Library)
-- Attached to bookings as media collections: 'before_photos', 'after_photos'

-- Memberships / Subscription Plans
membership_plans
  id, name, description,
  washes_per_month (null = unlimited),
  price_monthly, stripe_price_id,
  eligible_vehicle_sizes (jsonb),
  is_active,
  created_at, updated_at

-- Customer Memberships
customer_memberships
  id, user_id, plan_id,
  stripe_subscription_id, status,
  current_period_start, current_period_end,
  washes_used_this_period,
  created_at, updated_at

-- Fleet / Commercial Accounts
fleet_accounts
  id, company_name, contact_name, contact_email, contact_phone,
  billing_cycle (enum: monthly|weekly),
  discount_percent, net_days,
  stripe_customer_id,
  created_at, updated_at

fleet_account_vehicles
  id, fleet_account_id, vehicle_id,
  created_at

-- Service Zones (zip-code based for mobile)
service_zones
  id, location_id, zip_code, is_active,
  created_at, updated_at

-- Webhooks (outbound)
webhook_endpoints
  id, url, secret, events (jsonb), is_active,
  created_at, updated_at

webhook_deliveries
  id, endpoint_id, event, payload (jsonb),
  response_status, attempts, last_attempt_at,
  created_at

-- Notifications / Reminders log
notification_logs
  id, booking_id, user_id, channel (enum: sms|email|push),
  type, message, sent_at, status,
  created_at
```

---

## 4. User Roles & Permissions

| Role | Access |
|------|--------|
| `admin` | Full system access, settings, all reports, all locations |
| `manager` | Manage bookings, technicians, packages for assigned location |
| `technician` | View own schedule, update job status, upload photos |
| `customer` | Create bookings, manage own vehicles, view own history |

**Spatie Permissions setup:**
```php
// Roles
Role::create(['name' => 'admin']);
Role::create(['name' => 'manager']);
Role::create(['name' => 'technician']);
Role::create(['name' => 'customer']);

// Key permissions
'manage-locations', 'manage-packages', 'manage-technicians',
'view-all-bookings', 'manage-bookings', 'view-own-bookings',
'manage-fleet-accounts', 'view-reports', 'manage-zones',
'update-job-status', 'upload-job-photos'
```

---

## 5. Customer Booking Flow (7 Steps)

The booking wizard is an Inertia multi-step form. State persists in a `booking_draft` session. Each step is a Laravel controller that validates and advances the wizard.

### Step 1 — Select Service Type & Location
- **Option A — Mobile:** Customer enters address → Google Places Autocomplete → system checks `service_zones` for zip code match → if outside zone, show "not available" with waitlist option
- **Option B — Fixed:** Customer selects from list of active `locations`
- Controller: `BookingWizardController@location`

### Step 2 — Select or Add Vehicle
- Returning customers see saved vehicles, tap to select
- New customers fill: Make, Model, Year, Color, Size (Sedan/SUV/Truck/Van/Oversized)
- Vehicle size stored in session for pricing in Step 3
- Controller: `BookingWizardController@vehicle`

### Step 3 — Choose Wash Package
- Visual cards showing package name, description, duration, and price for their vehicle size
- Price pulled from `packages.price_{size}` column
- Controller: `BookingWizardController@package`

### Step 4 — Add-On Services
- Checkbox list of available add-ons
- Price and duration auto-calculated per vehicle size
- Running total shown in sidebar
- Controller: `BookingWizardController@addons`

### Step 5 — Pick Date & Time
- FullCalendar showing available slots
- Slot availability logic:
  - **Mobile:** Query technician availability minus booked time minus travel buffer (configurable per technician, default 20 min)
  - **Fixed:** Query bay/station availability at selected location
- Customer's local timezone shown
- Controller: `BookingWizardController@datetime`

### Step 6 — Customer Info & Special Notes
- Name, phone, email (pre-filled for logged-in customers)
- Mobile: confirm service address + gate code / parking instructions
- Custom intake form: "Any pre-existing damage we should note?"
- Guest checkout allowed — account silently created
- Controller: `BookingWizardController@customerInfo`

### Step 7 — Payment & Confirm
- Full payment OR deposit (configurable per package)
- Stripe Payment Intent created server-side
- Card saved to customer profile for future bookings / cancellation fee
- On success: booking confirmed, SMS + email sent, tracking link generated
- Controller: `BookingWizardController@payment`

---

## 6. Technician PWA (Mobile App)

Built as a Progressive Web App using Vite PWA plugin. Technicians install it on their phone from the browser.

### Features
- **Today's Jobs:** Ordered list of assigned jobs for the day with address, vehicle, package, add-ons
- **Job Status Updates (Finite State Machine):**
  - `confirmed` → `on_the_way` → `arrived` → `in_progress` → `completed`
  - Each transition broadcasts a Laravel event → Reverb → customer tracking page updates live
  - SMS sent to customer at `on_the_way` and `completed`
- **Photo Capture:**
  - Camera API via browser
  - Before photos required before marking `in_progress`
  - After photos required before marking `completed`
  - Uploaded via presigned S3 URL
- **Offline Support:** Service worker caches today's jobs so app works on poor signal

### Laravel Events Fired
```php
JobStatusUpdated::class      // broadcasts to customer tracking channel
TechnicianEnRoute::class     // triggers SMS "Your tech is on the way"
JobCompleted::class          // triggers SMS with photo review link
```

---

## 7. Real-Time Job Tracking Page

Public URL: `/track/{booking_uuid}` — no login required.

- Shows current job status with visual progress bar
- Live updates via Laravel Echo subscribing to `booking.{id}` private channel
- Displays: technician name, estimated arrival, before/after photos when available
- After completion: prompts customer to leave a review

---

## 8. Key Feature Implementations

### Service Zone Validation
```php
// BookingWizardController@location
$zip = $request->zip;
$available = ServiceZone::where('zip_code', $zip)
    ->where('is_active', true)
    ->exists();

if (!$available) {
    // Return zone not covered response
    // Offer: notify me when available (waitlist)
}
```

### Vehicle-Size Pricing
```php
// Package price resolver
public function priceForVehicle(Vehicle $vehicle): int
{
    return match($vehicle->size) {
        'sedan'     => $this->price_sedan,
        'suv'       => $this->price_suv,
        'truck'     => $this->price_truck,
        'van'       => $this->price_van,
        'oversized' => $this->price_oversized,
    };
}
```

### Travel Buffer Slot Generation
```php
// SlotGeneratorService
// For mobile bookings: fetch technician's bookings for the day
// Build unavailable windows: booking->scheduled_at to booking->ends_at + travel_buffer_minutes
// Return 30-min slots that don't overlap unavailable windows
```

### Weather Auto-Reschedule (Nightly Cron)
```php
// App\Console\Commands\CheckWeatherForMobileJobs
// Runs daily at 8pm for next-day jobs
// Calls OpenWeatherMap API for each mobile job's service zip
// If precipitation probability > 70%: send SMS with reschedule options
```

### Membership / Subscription
```php
// Using Laravel Cashier
$user->newSubscription('default', $plan->stripe_price_id)->create($paymentMethodId);

// Booking check: if customer has active membership, apply free wash
// Decrement washes_used_this_period on booking confirmation
```

---

## 9. Laravel Module Structure (nwidart/laravel-modules)

```
app/
Modules/
  Booking/          ← Core booking engine, wizard, calendar
  Vehicles/         ← Vehicle profiles, size pricing
  Locations/        ← Fixed locations, service zones
  Jobs/             ← Job status FSM, technician app data
  Payments/         ← Cashier, Stripe, invoices
  Notifications/    ← SMS, email, push templates
  Memberships/      ← Subscription plans, usage tracking
  Fleet/            ← Commercial accounts, bulk invoicing
  Weather/          ← OpenWeatherMap, reschedule automation
  Reports/          ← Revenue, technician, zone analytics
  Plugins/          ← Plugin registry, webhook outbound
```

Each module has its own: `routes/`, `Controllers/`, `Models/`, `Events/`, `Listeners/`, `database/migrations/`, `resources/js/Pages/`

---

## 10. Plugin System Architecture

Plugins are Laravel Service Providers auto-discovered via Composer.

### How a plugin registers itself
```php
// A "Loyalty Points" plugin example
class LoyaltyServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Register routes
        $this->loadRoutesFrom(__DIR__.'/../routes/loyalty.php');

        // Register migrations (namespaced, never touch core tables)
        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');

        // Listen to core events
        Event::listen(BookingCompleted::class, AwardLoyaltyPoints::class);

        // Share data to all Inertia responses
        Inertia::share('loyalty', fn() => auth()->user()?->loyaltyBalance());
    }
}
```

### Core Events plugins can hook into
```php
BookingCreated::class
BookingConfirmed::class
BookingCancelled::class
JobStatusUpdated::class
JobCompleted::class
PaymentCompleted::class
MembershipActivated::class
MembershipCancelled::class
CustomerRegistered::class
```

### Plugin-ready add-ons (Phase 5)
- **Loyalty Points** — earn points per wash, redeem for discounts
- **Gift Cards** — sell and redeem gift cards
- **Referral Program** — customer referral tracking and credits
- **Route Optimizer** — Google Directions API to sequence mobile jobs optimally
- **Review Automation** — post-completion review request (Google, Yelp)
- **Marketing Automation** — win-back campaigns for lapsed customers
- **White-Label / Franchise** — multi-tenant for multiple franchise owners

---

## 11. Build Phases

### Phase 1 — MVP Core (Weeks 1–8)
- Laravel 13 + Inertia + React starter kit setup
- Auth: customer registration/login, admin login
- Service zone setup (zip-code based)
- Fixed location branches (CRUD)
- Vehicle profiles (make/model/year/color/size)
- Wash packages + add-ons catalog with vehicle-size pricing
- 7-step booking wizard (all steps)
- Staff calendar view (FullCalendar)
- Basic technician assignment
- SMS + email booking confirmation (Twilio + Resend)
- Admin dashboard (booking list, basic stats)

### Phase 2 — Payments + Customer Portal (Weeks 9–14)
- Laravel Cashier + Stripe integration
- Deposits and full payment at booking
- Cards on file (Stripe SetupIntent)
- Cancellation fee enforcement (charge saved card)
- Auto-generated PDF invoices
- Customer self-service portal (manage vehicles, view bookings)
- Reschedule and cancel flow with policy enforcement
- Admin booking management (create, edit, reassign)

### Phase 3 — Technician App + Job Tracking (Weeks 15–20)
- Technician PWA (vite-plugin-pwa)
- Today's jobs view + status update UI
- Job FSM: on_the_way → arrived → in_progress → completed
- Before/after photo capture (browser Camera API → S3)
- Laravel Reverb WebSocket server setup
- Customer tracking page (`/track/{uuid}`) with live updates
- Real-time SMS: "on the way" + "job complete" messages
- Travel-time buffer logic in slot generator

### Phase 4 — Advanced Features (Weeks 21–28)
- Recurring bookings (weekly/bi-weekly)
- Monthly membership / subscription plans (Cashier)
- Fleet / commercial accounts (multi-vehicle, monthly invoicing)
- Google Maps zone validation (geo-radius upgrade from zip codes)
- Weather-based reschedule automation (OpenWeatherMap)
- Automation workflows (post-job review request, win-back SMS)
- Detailed reporting: revenue by zone, jobs per technician, package popularity
- Multi-location admin management

### Phase 5 — Plugin System + Scale (Weeks 29+)
- Plugin Service Provider architecture (nwidart/laravel-modules)
- Core event bus finalized (all hookable events documented)
- Webhook outbound system (BullMQ-style via Laravel Queue)
- Plugin marketplace UI in admin
- Public REST API with OAuth2 scoped tokens
- Loyalty points plugin
- Gift card plugin
- Route optimizer plugin (Google Directions)
- Laravel Octane setup for high-traffic production

---

## 12. Key Laravel Packages to Install

```bash
# Core
composer require inertiajs/inertia-laravel
composer require tightenco/ziggy                    # Named routes in React

# Auth & Permissions
composer require laravel/sanctum
composer require spatie/laravel-permission

# Payments
composer require laravel/cashier

# Real-time
composer require laravel/reverb
# npm install --save-dev laravel-echo pusher-js

# Queues
composer require laravel/horizon

# Modules (plugin architecture)
composer require nwidart/laravel-modules

# Media (photos)
composer require spatie/laravel-medialibrary

# Search
composer require laravel/scout
composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle

# Excel/PDF reports
composer require maatwebsite/excel
composer require barryvdh/laravel-dompdf

# Debugging (dev only)
composer require laravel/telescope --dev
```

```bash
# Frontend
npm install @inertiajs/react
npm install @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction
npm install zustand
npm install react-hook-form zod @hookform/resolvers
npm install laravel-echo pusher-js
npm install vite-plugin-pwa
npm install @googlemaps/js-api-loader
```

---

## 13. Environment Variables Needed

```env
# App
APP_NAME="AIO Car Wash"
APP_URL=https://yourdomain.com

# Database
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=carwash
DB_USERNAME=
DB_PASSWORD=

# Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

# Stripe
STRIPE_KEY=pk_live_...
STRIPE_SECRET=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
CASHIER_CURRENCY=usd

# Twilio
TWILIO_SID=
TWILIO_TOKEN=
TWILIO_FROM=+1...

# Resend (email)
RESEND_API_KEY=re_...
MAIL_FROM_ADDRESS=hello@yourdomain.com

# Google Maps
GOOGLE_MAPS_API_KEY=

# OpenWeatherMap
OPENWEATHER_API_KEY=

# AWS S3 / Cloudflare R2 (photos)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=carwash-photos
AWS_URL=

# Laravel Reverb (WebSockets)
REVERB_APP_ID=
REVERB_APP_KEY=
REVERB_APP_SECRET=
REVERB_HOST=localhost
REVERB_PORT=8080

# Meilisearch
MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_KEY=
```

---

## 14. Folder / File Conventions

```
app/
  Modules/
    Booking/
      Controllers/
        BookingWizardController.php   ← 7-step wizard
        BookingController.php         ← Admin CRUD
      Models/
        Booking.php
        BookingAddon.php
      Services/
        SlotGeneratorService.php      ← Available time slot logic
        BookingPricingService.php     ← Vehicle-size price resolver
      Events/
        BookingCreated.php
        BookingConfirmed.php
        BookingCancelled.php
      Listeners/
        SendBookingConfirmationSMS.php
        SendBookingConfirmationEmail.php
    Jobs/
      Models/
        JobStatusLog.php
      Services/
        JobStateMachineService.php    ← FSM: on_the_way → completed
      Events/
        JobStatusUpdated.php
        JobCompleted.php
    Vehicles/
      Models/
        Vehicle.php
    Locations/
      Models/
        Location.php
        ServiceZone.php
      Services/
        ZoneValidationService.php     ← Check if address/zip is in zone
    Weather/
      Commands/
        CheckWeatherForMobileJobs.php ← Nightly cron
      Services/
        WeatherService.php

resources/
  js/
    Pages/
      Booking/
        Wizard/
          Step1Location.tsx
          Step2Vehicle.tsx
          Step3Package.tsx
          Step4Addons.tsx
          Step5DateTime.tsx
          Step6CustomerInfo.tsx
          Step7Payment.tsx
      Admin/
        Dashboard.tsx
        Bookings/
          Index.tsx
          Show.tsx
        Calendar.tsx
        Locations/
        Packages/
        Technicians/
        Reports/
      Technician/
        Dashboard.tsx       ← PWA entry point
        JobDetail.tsx
        PhotoCapture.tsx
      Customer/
        Portal.tsx
        Vehicles/
        BookingHistory.tsx
      Track/
        Show.tsx            ← Public tracking page /track/{uuid}
    Components/
      Booking/
        PackageCard.tsx
        VehicleSizeSelector.tsx
        AddOnCheckbox.tsx
        ZoneChecker.tsx
      Calendar/
        BookingCalendar.tsx
      Map/
        AddressAutocomplete.tsx
        ServiceZoneMap.tsx
    Layouts/
      AdminLayout.tsx
      CustomerLayout.tsx
      TechnicianLayout.tsx  ← Minimal PWA layout
```

---

## 15. Notes for Cursor / AI Development

- Use **Inertia forms** (`useForm` from `@inertiajs/react`) for all wizard steps — handles validation errors from Laravel automatically
- Use **Laravel Form Requests** for all wizard step validation — keeps controllers clean
- The booking wizard stores state in `session('booking_draft')` between steps — never in the URL or client-only state
- All money values stored in **cents** (integers) in the database — never floats
- Job status uses a **Finite State Machine** — only allow valid transitions (e.g., can't go from `confirmed` directly to `completed`)
- All Reverb channel names follow: `bookings.{booking_id}` for job tracking, `admin.calendar` for live calendar updates
- The customer tracking page at `/track/{uuid}` is **public** — no auth required — use a UUID token on the booking, not the integer ID
- For photo uploads: generate **presigned S3 URLs** server-side, upload direct from browser — never route photos through Laravel app server
- Vehicle size pricing uses dedicated columns (not a JSON field) so prices are easily queryable and reportable
- **Always validate service zone before creating a booking** — use a database transaction wrapping slot reservation + booking creation to prevent race conditions
