# Car Rental Platform — Source of Truth
> Last updated: 2026-04-04
> Status: Core build complete. Pre-launch tasks remaining.

---

## 1. Project Overview

A self-hosted Laravel 11 car rental booking and management system built to replace VEVS Car Rental Software. Single company, two locations. Customers book via API (consumed by a separate existing Laravel marketing site). Staff manage everything through a Filament admin back office.

---

## 2. Business Context

| Detail | Value |
|---|---|
| Model | Single company (not multi-tenant) |
| Locations | 2 (City Centre, Airport) |
| Payment gateways | Stripe, PayPal |
| Customer-facing UI | Separate existing Laravel marketing site (calls this system's API) |
| Replacing | VEVS Car Rental Software |
| Data migration | VEVS CSV export → import command |

---

## 3. Tech Stack

| Layer | Choice |
|---|---|
| Framework | Laravel 11 |
| PHP | 8.2+ |
| Database | MySQL 8 |
| Admin back office | Filament 3 |
| Reactive UI | Livewire 3 + Alpine.js |
| Queue management | Laravel Horizon |
| Roles & permissions | Spatie Laravel Permission v6 |
| Audit log | Spatie Laravel Activity Log v4 |
| Media handling | Spatie Laravel Media Library v11 |
| Stripe | stripe/stripe-php v13 |
| PayPal | srmklive/paypal v3 |
| PDF generation | barryvdh/laravel-dompdf v2 |
| API auth | Laravel Sanctum |
| Testing | Pest v2 + pest-plugin-laravel |

---

## 4. Roles & Permissions

Three staff roles managed by Spatie Permission. Customers are not Laravel users by default (guest bookings supported); they optionally link to a User via `customer.user_id`.

| Role | Scope |
|---|---|
| `admin` | All permissions, all locations |
| `manager` | All booking/payment/report permissions at their location |
| `agent` | Create/view bookings, record payments, conduct inspections |

Key permissions: `bookings.*`, `vehicles.*`, `customers.*`, `payments.*`, `inspections.*`, `reports.view`, `settings.manage`, `users.manage`, `pricing.manage`, `locations.manage`.

---

## 5. Database Schema

All monetary values stored as **integers in cents/pence**. Soft deletes on `bookings`, `vehicles`, `customers`.

### Tables (15 total, migration order)

| # | Table | Key columns / notes |
|---|---|---|
| 1 | `locations` | name, address, city, postcode, lat, lng, opening_hours (JSON), is_active |
| 2 | `vehicle_categories` | name, slug, sort_order, is_active |
| 3 | `vehicles` | category_id, location_id (home), make, model, year, colour, registration_plate (unique), vin, seats, doors, transmission, fuel_type, mileage, status (available/rented/maintenance/retired), soft_delete |
| 4 | `vehicle_features` + `vehicle_feature_vehicle` | pivot — BelongsToMany |
| 5 | `vehicle_images` | vehicle_id, path, is_primary, sort_order |
| 6 | `users` | location_id (branch), role via Spatie, is_active |
| 7 | `customers` | user_id (nullable), email (unique), licence_number, licence_expiry, licence_country, is_blacklisted, soft_delete |
| 8 | `extras` | name, price_per_day (cents), price_flat (cents), max_quantity, is_active |
| 9 | `pricing_rules` | vehicle_id (nullable), vehicle_category_id (nullable), type (daily/hourly/weekly), price (cents), min_days, max_days, starts_at, ends_at, priority, is_active |
| 10 | `promo_codes` | code (unique), discount_type (percent/fixed), discount_value, min_booking_amount, max_uses, used_count, valid_from, valid_until |
| 11 | `bookings` | reference (BK-YYYY-NNNNN), customer_id, vehicle_id, pickup/dropoff location_id, status (pending/confirmed/active/completed/cancelled/no_show), all amounts in cents, soft_delete |
| 12 | `booking_extras` | booking_id, extra_id, quantity, unit_price, total_price (snapshot at booking time) |
| 13 | `payments` | booking_id, type (deposit/balance/refund/manual/adjustment), gateway (stripe/paypal/cash/bank_transfer), gateway_transaction_id, gateway_payment_intent_id, amount, status |
| 14 | `inspections` + `inspection_damages` + `inspection_photos` | one pickup + one dropoff per booking, fuel_level (1–8), damages with location/severity/pre_existing/repair_cost |
| 15 | `rental_agreements` | booking_id (unique), content (HTML snapshot), customer_signature (path), signed_at, sent_at, viewed_at |

Plus: Spatie permission tables (`roles`, `permissions`, `model_has_roles`, etc.) via vendor publish.

### Key Relationships

```
Location         → many Vehicles (home location)
Location         → many Bookings (pickup / dropoff)
Vehicle          → one Category
Vehicle          → many Images, Features (pivot)
Vehicle          → many Bookings, PricingRules, Inspections
Booking          → one Customer, one Vehicle
Booking          → one PickupLocation, one DropoffLocation
Booking          → many Extras (via booking_extras pivot)
Booking          → many Payments
Booking          → two Inspections (pickup + dropoff)
Booking          → one RentalAgreement
Customer         → one User (optional, nullable)
PricingRule      → one Vehicle OR one Category (or global if both null)
```

---

## 6. Services

All services are singletons, bound in `AppServiceProvider`.

### BookingService
`app/Services/BookingService.php`

Core responsibilities:
- `getAvailableVehicles(pickup, dropoff, locationId, ?categoryId)` — returns vehicles with no overlapping confirmed/active bookings
- `calculatePrice(vehicle, pickup, dropoff, extras[], ?promoCode)` — returns full price breakdown array
- `createBooking(...)` — atomic creation with pessimistic DB lock to prevent race conditions; attaches extras, increments promo usage
- Status transitions: `confirm()`, `activate()`, `complete()`, `cancel()`
- Notification hooks: `confirm()` fires `BookingConfirmed`; `cancel()` fires `BookingCancelled`

### PricingService
`app/Services/PricingService.php`

Rule resolution priority (highest wins):
1. Vehicle-specific rule matching date window
2. Vehicle-specific base rule
3. Category seasonal rule matching date window
4. Category base rule

Rate types: `daily` (price × days), `weekly` (full weeks + daily remainder), `hourly`.
`getDisplayRate(vehicle)` returns base daily rate for listing pages.

### PaymentService
`app/Services/PaymentService.php`

- **Stripe**: `createStripeDepositIntent()`, `createStripeBalanceIntent()`, `handleStripeWebhook()`, `refundStripe()`
- **PayPal**: `createPayPalDepositOrder()`, `capturePayPalOrder()`
- **Manual**: `recordManualPayment()` (cash / bank_transfer)
- Webhook handler auto-confirms booking when deposit received
- Fires `PaymentReceived` notification on successful payment

### RentalAgreementService
`app/Services/RentalAgreementService.php`

- `generate(booking)` — renders Blade template to HTML snapshot, stores PDF, emails customer a tamper-proof signed URL (expires 1hr after pickup)
- `sign(booking, signatureData)` — stores base64 PNG signature, re-renders PDF with signature
- `markViewed(agreement)` — records when customer opens the sign link
- `renderPdf(booking, agreement)` — DomPDF render to `storage/app/agreements/`
- `downloadPdf(booking)` — returns raw PDF bytes for streaming

### InspectionService
`app/Services/InspectionService.php`

- `create(booking, data)` — creates inspection, stores photos, attaches damages, triggers `activate()` on pickup or `complete()` on dropoff, updates vehicle mileage
- `generateSheet(inspection)` — renders PDF inspection sheet
- `compareInspections(booking)` — diffs pickup vs dropoff damages, returns new damages + fuel difference + mileage driven

---

## 7. API Endpoints

Base path: `/api`
Auth: Laravel Sanctum (`auth:sanctum` middleware)

### Public (no auth)
| Method | Route | Controller | Notes |
|---|---|---|---|
| GET | `/availability` | AvailabilityController@index | Params: pickup_datetime, dropoff_datetime, pickup_location_id, ?category_id |
| GET | `/vehicles/{vehicle}` | AvailabilityController@vehicle | |
| POST | `/promo-codes/validate` | AvailabilityController@validatePromo | |
| POST | `/webhooks/stripe` | PaymentController@stripeWebhook | Verified by Stripe signature |

### Authenticated
| Method | Route | Notes |
|---|---|---|
| GET | `/bookings` | Customer's own bookings, paginated |
| POST | `/bookings` | Create booking (StoreBookingRequest validation) |
| GET | `/bookings/{booking}` | Single booking with all relations |
| POST | `/bookings/{booking}/cancel` | Requires `reason` |
| GET/PUT | `/customers/{customer}` | Own profile only |
| POST | `/bookings/{booking}/payment/stripe/deposit` | Returns client_secret |
| POST | `/bookings/{booking}/payment/stripe/balance` | Returns client_secret |
| POST | `/bookings/{booking}/payment/paypal/deposit` | Returns order_id + approve_url |
| GET | `/bookings/{booking}/payment/paypal/capture` | PayPal redirect after approval |

### Web routes (signed URLs)
| Method | Route | Notes |
|---|---|---|
| GET | `/agreements/{reference}/sign` | Tamper-proof signed URL, expires 1hr after pickup |
| POST | `/agreements/{reference}/sign` | Submits base64 signature |
| GET | `/agreements/{reference}/download` | PDF download (auth required) |

---

## 8. Booking Reference Format

`BK-{YEAR}-{NNNNN}` e.g. `BK-2026-00142`

Sequential within the calendar year, zero-padded to 5 digits. Generated in `Booking::booted()`.

---

## 9. Booking Status Flow

```
pending → confirmed → active → completed
    ↓           ↓         ↓
 cancelled   cancelled  cancelled
                              ↓
                           no_show
```

- `pending` — created, awaiting payment
- `confirmed` — deposit received
- `active` — vehicle handed over (pickup inspection done)
- `completed` — vehicle returned (dropoff inspection done)
- `cancelled` — can be cancelled from pending, confirmed, or active
- `no_show` — confirmed but customer never collected

---

## 10. Pricing Rules Logic

- Rules can be scoped to a specific vehicle, a category, or global (both nullable).
- Priority field: higher number wins.
- Seasonal rules have `starts_at` / `ends_at`; base rules have both null.
- Weekly discount: seeded as 7+ day rule at 15% off base × 7.
- `PricingService::resolveRule()` finds the single best match.
- Price calculated as: base + extras → subtract discount → add VAT → deposit = 30% of total.

### Seeded base rates (pence/day)
| Category | Daily | Weekly |
|---|---|---|
| Economy | £45 | ~£267 (15% off) |
| Compact | £55 | ~£327 |
| Saloon | £65 | ~£386 |
| Estate | £70 | ~£416 |
| SUV | £85 | ~£505 |
| Minivan | £90 | ~£535 |
| Luxury | £150 | ~£892 |
| Electric | £75 | ~£446 |

---

## 11. Config: rental.php

| Key | Default | Notes |
|---|---|---|
| `rental.currency` | `GBP` | ISO 4217 |
| `rental.tax_rate` | `0.20` | 20% VAT |
| `rental.deposit_rate` | `0.30` | 30% upfront |
| `rental.min_rental_days` | `1` | |
| `rental.hold_minutes` | `30` | Auto-cancel unpaid pending bookings |
| `rental.company.*` | — | name, email, phone, address, vat_no |

---

## 12. Notifications (all queued via Horizon)

| Class | Trigger | Channel |
|---|---|---|
| `BookingConfirmed` | Deposit received / staff confirms | Mail |
| `BookingCancelled` | Any cancellation | Mail |
| `BookingReminder` | 24hrs before pickup (scheduled) | Mail |
| `RentalAgreementReady` | Agreement generated | Mail |
| `PaymentReceived` | Any successful payment | Mail |

---

## 13. Scheduled Jobs (routes/console.php)

| Command | Schedule | Purpose |
|---|---|---|
| `bookings:send-reminders` | Daily 09:00 | 24hr pickup reminder emails |
| `bookings:auto-cancel` | Every 5 mins | Cancel unpaid pending bookings after hold window |
| `activitylog:clean --days=180` | Monthly | Prune old audit log entries |
| `horizon:snapshot` | Every 5 mins | Horizon metrics |

---

## 14. Filament Admin

### Navigation structure
```
Operations
  ├── Bookings         (BookingResource)
  ├── Customers        (CustomerResource)
  └── Inspections      (InspectionResource)
Fleet
  └── Vehicles         (VehicleResource)
Analytics
  └── Reports          (Reports page)
```

### Dashboard widgets
1. `KpiStatsWidget` — Revenue today, Active rentals, Fleet utilisation %, Today's pickups
2. `RevenueChartWidget` — 30-day daily revenue bar chart
3. `UpcomingPickupsWidget` — Confirmed bookings in next 48hrs, shows outstanding balance
4. `UpcomingReturnsWidget` — Active bookings due back in 48hrs, overdue flagged red

### Reports page
- Filterable by period (today / this week / this month / last month / this year / custom range) and by location
- KPI cards: gross revenue, refunds, net revenue, total bookings, completed, cancelled, avg booking value
- Daily revenue bar chart (Chart.js)
- Vehicle utilisation table with inline progress bar (green ≥70%, amber ≥40%, red <40%)
- Top 10 customers by spend

### Filament resources built
`BookingResource`, `VehicleResource`, `CustomerResource`, `InspectionResource`

### Still to build in Filament
`ExtraResource`, `PricingRuleResource`, `PromoCodeResource`, `LocationResource`, `UserResource`

---

## 15. VEVS Data Migration

Command: `php artisan vevs:migrate`

Options:
- `--step=all|customers|vehicles|bookings|payments`
- `--dry-run` — preview without writing
- `--file=` — override default CSV filename

CSV files expected in `storage/app/vevs-import/`:
| Step | Default filename |
|---|---|
| customers | `clients.csv` |
| vehicles | `fleet.csv` |
| bookings | `reservations.csv` |

Key behaviours:
- **Idempotent** — safe to re-run; skips existing records matched by email (customers) and registration plate (vehicles)
- Bookings matched by `internal_notes LIKE '%VEVS:{ref}%'`
- Handles flexible column naming (VEVS export headers vary)
- Parses dates in multiple formats: `d/m/Y`, `Y-m-d`, `m/d/Y`, `d-m-Y`, `d.m.Y`
- Maps VEVS statuses to internal statuses
- Creates synthetic payment records for migrated bookings

---

## 16. Test Suite (Pest)

18 tests across 5 describe blocks. All use `RefreshDatabase`.

| File | Tests |
|---|---|
| `tests/Unit/PricingServiceTest.php` | Base rate, vehicle override, seasonal active, seasonal inactive, weekly partial days, missing rule throws |
| `tests/Feature/BookingAvailabilityTest.php` | Returns vehicles, excludes overlapping, includes cancelled, adjacent boundary |
| `tests/Feature/BookingCreationTest.php` | Correct financials, unique reference, promo code, unavailable throws, full lifecycle, cancel frees vehicle |
| `tests/Unit/PromoCodeTest.php` | Valid, expired, usage limit, percent discount, fixed discount, capped discount |
| `tests/Feature/ApiAvailabilityTest.php` | Returns vehicles, missing fields 422, past pickup 422 |

### Factories (9)
`LocationFactory`, `VehicleCategoryFactory`, `VehicleFactory`, `CustomerFactory`, `UserFactory`, `BookingFactory`, `PricingRuleFactory`, `PromoCodeFactory`, `PaymentFactory`, `ExtraFactory`

State methods: `->pending()`, `->active()`, `->completed()`, `->cancelled()`, `->rented()`, `->unavailable()`, `->weekly()`, `->seasonal(from, until)`, `->expired()`, `->fixed(cents)`, `->refund()`

---

## 17. File Manifest

All generated files. Each consolidated file must be split into individual class files when placed in the project.

| Generated file | Split into | Location |
|---|---|---|
| `001_locations_vehicles.php` | 5 migration files | `database/migrations/` |
| `002_users_customers_pricing.php` | 5 migration files | `database/migrations/` |
| `003_bookings_payments_inspections.php` | 5 migration files | `database/migrations/` |
| `Models.php` | 16 model files | `app/Models/` |
| `BookingService.php` | 1 file | `app/Services/` |
| `PricingService.php` | 1 file | `app/Services/` |
| `PaymentService.php` | 1 file | `app/Services/` |
| `RentalAgreementService.php` | 1 file + 1 Blade view | `app/Services/` + `resources/views/agreements/` |
| `InspectionService.php` | 1 file + 1 Blade view | `app/Services/` + `resources/views/inspections/` |
| `Controllers.php` | 4 controller files + routes | `app/Http/Controllers/Api/` + `routes/` |
| `AgreementController.php` | 1 controller + 1 Blade view + route additions | `app/Http/Controllers/` + `resources/views/agreements/` + `routes/web.php` |
| `AdminResources.php` | 3 Filament resource files | `app/Filament/Resources/` |
| `DashboardWidgets.php` | 1 page + 4 widget files | `app/Filament/Pages/` + `app/Filament/Widgets/` |
| `Reports.php` | 1 page + 1 Blade view | `app/Filament/Pages/` + `resources/views/filament/pages/` |
| `DatabaseSeeder.php` | 8 seeder files | `database/seeders/` |
| `Notifications.php` | 5 notification files | `app/Notifications/` |
| `ScheduledCommands.php` | 2 command files + `routes/console.php` additions | `app/Console/Commands/` + `routes/` |
| `MigrateVevsData.php` | 1 command file | `app/Console/Commands/` |
| `StoreBookingRequest.php` | 1 request + 1 policy + `AppServiceProvider` + `AuthServiceProvider` additions | `app/Http/Requests/` + `app/Policies/` + `app/Providers/` |
| `PestTestSuite.php` | 5 test files | `tests/Unit/` + `tests/Feature/` |
| `Factories.php` | 10 factory files | `database/factories/` |
| `PROJECT_STRUCTURE.md` | `config/rental.php` + `.env.example` + `composer.json` | `config/` + root |

---

## 18. Remaining / Not Yet Built

### Filament resources not yet generated
- `ExtraResource` — manage rental extras (GPS, child seats, etc.)
- `PricingRuleResource` — create/edit pricing rules and seasonal rates
- `PromoCodeResource` — manage promo codes
- `LocationResource` — manage the two branches
- `UserResource` — manage staff accounts

### Other gaps
- `VehicleResource` image upload UI (Spatie Media Library integration)
- Customer document upload (driving licence, passport scan)
- `BookingResource` Filament action buttons: Confirm, Activate, Complete, Cancel (currently status is editable via form only)
- Reporting CSV/Excel export
- API rate limiting
- API resource classes (`BookingResource`, `VehicleResource` JSON transformers — referenced but not fully written)
- `CustomerController` API (referenced in routes, not fully written)

### Pre-launch checklist
- [ ] Update `.env` with real Stripe/PayPal keys
- [ ] Update `config/rental.php` with real company details
- [ ] Set `RENTAL_CURRENCY`, `RENTAL_TAX_RATE`, `RENTAL_DEPOSIT_RATE`
- [ ] Export CSVs from VEVS and run `php artisan vevs:migrate --dry-run`
- [ ] Run migration for real: `php artisan vevs:migrate`
- [ ] Set up Stripe webhook endpoint in Stripe dashboard → `/api/webhooks/stripe`
- [ ] Configure queue worker / Horizon in production
- [ ] Set up scheduler: `* * * * * php artisan schedule:run`
- [ ] Configure mail driver (Mailgun recommended)
- [ ] `php artisan storage:link`
- [ ] Point marketing site booking CTA at `/api/availability`
- [ ] Run full test suite: `php artisan test`

---

## 19. Key Decisions & Conventions

- **Money in cents** — all monetary DB columns are unsigned integers. Display with `number_format($value / 100, 2)`.
- **No double-booking** — `createBooking()` uses a pessimistic lock (`lockForUpdate`) on the vehicle row inside a DB transaction.
- **Soft deletes** — Bookings, Vehicles, Customers use `SoftDeletes`. Never hard-delete these.
- **Guest bookings** — `Customer` does not require a linked `User`. Staff-created bookings set `created_by`; self-serve leaves it null.
- **Atomic reference generation** — `Booking::generateReference()` is year-scoped and sequential.
- **Agreements are HTML snapshots** — the agreement content is rendered and frozen at generation time so future template changes don't affect signed agreements.
- **Stripe webhooks confirm bookings** — the HTTP response returns immediately; booking confirmation happens asynchronously via webhook.
- **All notifications are queued** — nothing blocks the HTTP request. Requires Horizon running.
- **VEVS migration is idempotent** — matching on email and plate means re-running is safe.
