# Vehicle Module Analysis

## Scope

This document describes how the current Vehicle module works across database schema, Laravel models and services, admin create/edit flows, public listing and purchase flows, and the dynamic attributes/features systems.

This is an analysis-only reference. It does not propose destructive changes and it assumes backward compatibility is required.

## 1. Database Structure

### Overview

The vehicle system is built around a hybrid model:

- A legacy `vehicles` table with direct columns such as `fuel_type`, `transmission`, `color`, `vehicle_type`, and `engine_capacity`
- A normalized catalog hierarchy using `makes` and `models`
- A dynamic attribute system using `attributes`, `attribute_values`, `model_attributes`, and `vehicle_attribute_values`
- A feature flag system stored as JSON in `vehicles.features_json`

### Core Tables

#### `vehicles`

Purpose:

- Primary table for listings, pricing, inventory status, seller association, and backward-compatible legacy specification columns

Important columns:

| Column | Purpose |
| --- | --- |
| `id` | Primary key |
| `seller_id` | Nullable/legacy seller ownership reference to `users.id` |
| `make` | Legacy make name string |
| `model` | Legacy model name string |
| `make_id` | Normalized make FK to `makes.id` |
| `model_id` | Normalized model FK to `models.id` |
| `model_code` | Model code/chassis family code |
| `rec_no` | Internal record number |
| `chassis` | Chassis identifier |
| `grade` | Grade/trim text |
| `registration_year` | Registration year string |
| `manufacture_year` | Manufacturing year |
| `year` | Listing year |
| `mileage` | Mileage |
| `price` | Base price |
| `discount_percentage` | Discount percentage |
| `discount_amount` | Discount amount |
| `is_featured` | Featured listing flag |
| `is_clearance` | Clearance listing flag |
| `condition` | `new` / `used` |
| `vehicle_type` | Legacy body type column |
| `color` | Legacy color column |
| `transmission` | Legacy transmission column |
| `fuel_type` | Legacy fuel type column |
| `engine_capacity` | Legacy engine capacity column |
| `length_m` / `width_m` / `height_m` / `volume_m3` | Dimension fields |
| `seats` / `doors` | Legacy seat/door columns |
| `location` | Location text |
| `stock_country` | Stock country |
| `primary_image` | Main image path |
| `stock_status` | Inventory state: `available` / `sold` |
| `status` | Publishing/lifecycle state: `draft`, `published`, `reserved`, `sold` |
| `features_json` | JSON feature flags |
| `created_at` / `updated_at` | Timestamps |

Relationships:

- Belongs to `users` through `seller_id`
- Belongs to `makes` through `make_id`
- Belongs to `models` through `model_id`
- Has many `vehicle_attribute_values`
- Has many `vehicle_images`
- Has many `vehicle_documents`
- Has many `orders` and `shipments`

Schema sources:

- `database/migrations/2026_04_07_195624_create_vehicles_table.php`
- `database/migrations/2026_04_07_222633_add_extra_fields_to_vehicles_table.php`
- `database/migrations/2026_04_08_170327_add_discount_fields_to_vehicles_table.php`
- `database/migrations/2026_04_08_171020_add_type_and_stock_country_to_vehicles_table.php`
- `database/migrations/2026_04_09_000001_add_status_and_features_to_vehicles_table.php`
- `database/migrations/2026_04_10_000002_create_vehicle_catalog_tables.php`
- `database/migrations/2026_04_15_000007_add_model_code_to_vehicles_table.php`

#### `makes`

Purpose:

- Normalized vehicle make catalog

Columns:

- `id`
- `name`
- `created_at`
- `updated_at`

Relationships:

- Has many `models`
- Referenced by `vehicles.make_id`
- Referenced by `make_attributes.make_id`

#### `models`

Purpose:

- Normalized vehicle model catalog under a make

Columns:

- `id`
- `make_id`
- `name`
- `created_at`
- `updated_at`

Relationships:

- Belongs to `makes`
- Referenced by `vehicles.model_id`
- Referenced by `model_attributes.model_id`
- Referenced by `model_attribute_defaults.model_id`

### Dynamic Attribute Tables

#### `attributes`

Purpose:

- Master definition of dynamic attribute types such as Body Type, Fuel Type, Transmission, Color, Engine CC, and other catalog-driven specs

Columns:

- `id`
- `name`
- `type` (`select`, `multi_select`, `range`, `boolean`)
- `created_at`
- `updated_at`

Relationships:

- Has many `attribute_values`
- Referenced by `model_attributes`, `make_attributes`, and `vehicle_attribute_values`

#### `attribute_values`

Purpose:

- Stores selectable values for attributes of type `select` and `multi_select`

Columns:

- `id`
- `attribute_id`
- `value`
- `created_at`
- `updated_at`

Relationships:

- Belongs to `attributes`
- Referenced by `vehicle_attribute_values.value_id`
- Referenced by `model_attributes.default_value_id`
- Referenced by `make_attributes.default_value_id`
- Referenced by `model_attribute_defaults.value_id`

#### `model_attributes`

Purpose:

- Maps allowed attributes to a specific model and stores model-level metadata and defaults

Columns:

- `id`
- `model_id`
- `attribute_id`
- `min_numeric_value` / `max_numeric_value` (legacy numeric bounds)
- `min_value` / `max_value` (current numeric bounds)
- `default_value_id`
- `default_raw_value`
- `default_value` (JSON payload, including allowed value IDs)
- `created_at`
- `updated_at`

Relationships:

- Belongs to `models`
- Belongs to `attributes`
- Optional default relationship to `attribute_values`

#### `make_attributes`

Purpose:

- Make-level fallback attribute configuration when model-specific mapping is missing or incomplete

Columns:

- `id`
- `make_id`
- `attribute_id`
- `min_value`
- `max_value`
- `default_value_id`
- `default_raw_value`
- `created_at`
- `updated_at`

Relationships:

- Belongs to `makes`
- Belongs to `attributes`
- Optional default relationship to `attribute_values`

#### `model_attribute_defaults`

Purpose:

- Read-only/default snapshot table for model-level default selections

Columns:

- `id`
- `model_id`
- `attribute_id`
- `value_id`
- `raw_value`
- `created_at`
- `updated_at`

Relationships:

- Belongs to `models`
- Belongs to `attributes`
- Optional belongs to `attribute_values`

#### `vehicle_attribute_values`

Purpose:

- Stores the actual selected dynamic attribute values for each vehicle

Columns:

- `id`
- `vehicle_id`
- `attribute_id`
- `value_id` (nullable)
- `raw_value` (nullable)
- `created_at`
- `updated_at`

Relationships:

- Belongs to `vehicles`
- Belongs to `attributes`
- Optional belongs to `attribute_values`

Storage pattern:

- `select`: one row with `value_id`
- `multi_select`: multiple rows, one per selected `value_id`
- `range`: one row with `raw_value`
- `boolean`: one row with `raw_value` as `'1'` or `'0'`

### Related Vehicle Asset Tables

#### `vehicle_images`

Purpose:

- Stores additional gallery images per vehicle

Typical columns:

- `id`
- `vehicle_id`
- `image_path`
- `created_at`
- `updated_at`

Relationship:

- Belongs to `vehicles`

#### Features storage

There is no separate features table. Features are stored inline as JSON in `vehicles.features_json`.

## 2. Vehicle Model (Laravel)

Primary model file:

- `app/Models/Vehicle.php`

### Relationships

The Vehicle model currently exposes these core relationships:

- `seller()` -> `BelongsTo(User::class, 'seller_id')`
- `vehicleMake()` -> `BelongsTo(Make::class, 'make_id')`
- `vehicleModel()` -> `BelongsTo(VehicleModel::class, 'model_id')`
- `attributeValues()` -> `HasMany(VehicleAttributeValue::class)`
- `attributes()` -> `BelongsToMany(VehicleAttribute::class, 'vehicle_attribute_values')` with pivot fields `value_id` and `raw_value`
- `images()` -> `HasMany(VehicleImage::class)`
- `documents()` -> `HasMany(VehicleDocument::class)`
- `orders()` -> `HasMany(Order::class)`
- `shipments()` -> `HasMany(Shipment::class)`
- `availableModelAttributes()` -> `BelongsToMany(VehicleAttribute::class, 'model_attributes', ...)`

### Casts and Appended Attributes

Important casts:

- `features_json` -> `array`

Important appended attributes:

- `primary_image_url`
- `price_with_discount`
- `route_key`
- `feature_labels`

### Accessors / Compatibility Layer

The model intentionally preserves backward compatibility with legacy columns using accessors that resolve dynamic attribute values first and fall back to legacy fields.

Examples:

- `getColorAttribute()`
- `getVehicleTypeAttribute()`
- `getTransmissionAttribute()`
- `getFuelTypeAttribute()`
- `getEngineCapacityAttribute()`

This means customer-facing pages can continue reading `vehicle.color`, `vehicle.vehicle_type`, `vehicle.transmission`, `vehicle.fuel_type`, and `vehicle.engine_capacity` even if the real source is now `vehicle_attribute_values`.

### Boot Hooks

The Vehicle model uses boot hooks for key internal behavior:

- On `creating`: generate a unique `rec_no`
- On `saving`: synchronize legacy `make` and `model` string columns from `make_id` / `model_id`

This is important because many search and display flows still rely on the string columns even though normalized FKs now exist.

### Route Key Behavior

Vehicle routes use a slug-plus-encoded-ID route key. The model provides:

- `getRouteKey()`
- `resolveRouteBinding()`

This keeps public and admin URLs readable while still resolving the real database record safely.

### Dynamic Attribute Persistence Hooks

The Vehicle model owns dynamic attribute persistence through:

- `syncDynamicAttributes(array $attributes)`
- `storeDynamicAttributeValue(array $attributeValue)`

Behavior:

- Deletes existing vehicle attribute rows first
- Writes new `vehicle_attribute_values`
- Handles array `value_id` for multi-select attributes
- Skips rows where both `value_id` and `raw_value` are empty
- Re-syncs deprecated compatibility columns afterward
- Re-syncs `engine_capacity` from the `Engine CC` attribute

## 3. Dynamic Attributes System

### How Attributes Are Defined

Attributes are defined centrally in:

- `attributes`
- `attribute_values`

Admin CRUD is handled by:

- `app/Http/Controllers/Admin/AttributeController.php`
- `app/Http/Controllers/Admin/AttributeValueController.php`

Each attribute has a type:

- `select`
- `multi_select`
- `range`
- `boolean`

### How Attributes Are Assigned to Make/Model

Model-level assignment is stored in `model_attributes`.

Make-level fallback assignment is stored in `make_attributes`.

The service that merges and resolves those rules is:

- `app/Services/VehicleCatalogAttributeService.php`

Key behavior:

- Starts from model attributes when available
- Falls back to make-level attributes when needed
- Deduplicates by `attribute_id`
- Carries default metadata, min/max bounds, and allowed values

### How Values Are Stored for Vehicles

Vehicle selections are persisted into `vehicle_attribute_values`.

Patterns:

- `select`: one row, `value_id` populated
- `multi_select`: multiple rows with same `attribute_id`, different `value_id`
- `range`: one row, `raw_value` populated
- `boolean`: one row, `raw_value` set to `'1'` or `'0'`

### How Attributes Are Used in Queries

The main filtering engine is:

- `app/Services/VehicleFilterService.php`

Important behavior:

- Builds public listing queries using both legacy columns and dynamic attribute joins
- Supports attribute-based filtering using joins to `vehicle_attribute_values`
- Supports dynamic range and boolean facets
- Provides make/model/stock-country/price/mileage/year facets for the public catalog

The Vehicle model also exposes:

- `scopeApplyCatalogFilters()`

This is another attribute-aware query helper for filtering vehicle queries by catalog specs.

### How Attributes Are Used in Forms

The shared form component is:

- `resources/js/Components/Vehicles/VehicleForm.vue`

Current admin/seller create and edit pages all reuse this component.

Flow:

1. User chooses Make
2. Models load from `vehicle-catalog.makes.models`
3. User chooses Model
4. Dynamic attributes load from `/api/models/{model}/attributes`
5. Form structures a `dynamic_attributes` payload
6. Backend request classes decode and normalize that payload before saving

## 4. Features System

### Storage

Features are stored in:

- `vehicles.features_json`

This is a JSON object or array-like boolean map.

There is no normalized features table.

### Definition

Feature keys are hardcoded in:

- `app/Models/Vehicle.php`

Examples include:

- `cd_player`
- `sunroof`
- `abs`
- `airbags`
- `back_camera`
- `push_start`
- `esc`
- `navigation`

### Usage

Features are used mainly for:

- Admin/seller create and edit forms
- Public detail page display through `feature_labels`

### Difference From Dynamic Attributes

Features:

- Flat boolean flags
- No model-level configuration
- No separate value table
- Stored inline on the vehicle row

Dynamic attributes:

- Fully structured
- Typed (`select`, `multi_select`, `range`, `boolean`)
- Configurable per make/model
- Used for catalog filtering and compatibility fallbacks

## 5. Admin Create/Edit Flow

### Vue Form Structure

Admin create page:

- `resources/js/Pages/Admin/Vehicles/Create.vue`

Admin edit wrapper page:

- `resources/js/Pages/Vehicles/Edit.vue`

Shared component used by both:

- `resources/js/Components/Vehicles/VehicleForm.vue`

Current simplified admin-side structure is:

1. Seller
2. Basic Info
3. Vehicle Details / or condition-only admin-safe subset depending on mode
4. Location & Status
5. Specifications (dynamic attributes)
6. Features
7. Images

### Dynamic Attributes Payload Structure

The frontend currently builds dynamic attributes as an array of objects similar to:

```json
[
  {
    "attribute_id": 5,
    "value_id": 11,
    "raw_value": null,
    "is_required": true
  },
  {
    "attribute_id": 8,
    "value_id": null,
    "raw_value": "1500",
    "is_required": true
  }
]
```

Before submission, the form serializes only rows that actually have values.

### Backend Processing

Admin controller:

- `app/Http/Controllers/Admin/VehicleController.php`

The save pipeline is:

1. Request class validates and normalizes core fields
2. `dynamic_attributes` JSON string is decoded in the request class
3. Empty dynamic attribute rows are filtered out in `prepareForValidation()`
4. Controller extracts `dynamic_attributes` from validated input
5. Vehicle row is created or updated
6. `Vehicle::syncDynamicAttributes()` writes `vehicle_attribute_values`

### Validation Rules

Current request classes:

- `app/Http/Requests/StoreVehicleRequest.php`
- `app/Http/Requests/UpdateVehicleRequest.php`

Important current behavior:

- Core fields are still validated strictly
- `seller_id` is nullable for admin
- `dynamic_attributes` is nullable
- Empty dynamic attributes are filtered out before save
- Filled dynamic attributes are still validated for type and allowed value integrity
- Missing dynamic attributes no longer force an error after the recent admin-safe relaxation

## 6. Frontend Usage

### Public Listing / Search Page

Controller:

- `app/Http/Controllers/PublicController.php`

Page:

- `resources/js/Pages/Public/Home.vue`

How vehicle data is used:

- Uses `VehicleFilterService` to build public listing query
- Loads `images`, `attributeValues.attribute`, and `attributeValues.value`
- Uses make/model/model_code/price/year/mileage/stock_country facets
- Public search still depends on both legacy columns and dynamic attributes

### Public Vehicle Detail Page

Controller:

- `app/Http/Controllers/PublicController.php::show()`

Page:

- `resources/js/Pages/Public/VehicleShow.vue`

How vehicle data is used:

- Reads legacy-style fields directly: `vehicle_type`, `color`, `transmission`, `fuel_type`, `engine_capacity`, `doors`, `seats`
- Displays features via `feature_labels`
- Displays images and pricing
- Relies heavily on Vehicle model accessors for compatibility

### Checkout / Purchase Flow

Main controller:

- `app/Http/Controllers/Customer/CheckoutController.php`

Also relevant:

- `app/Http/Controllers/Customer/OrderController.php`
- `app/Services/CustomerPurchaseService.php`
- `app/Services/PricingService.php`
- `app/Services/ShippingService.php`

How vehicle data is used:

- Purchase page blocks unavailable vehicles
- Checkout uses `vehicle_id` from carts/order items
- Orders and purchase records reference vehicles directly
- Customer order views eager-load `items.vehicle.attributeValues.attribute` and `items.vehicle.attributeValues.value`

This means customer-side order and purchase flows still depend on vehicle records and their dynamic attributes being persisted consistently.

## 7. Duplication Analysis

### Legacy Columns vs Dynamic Attributes

These specifications exist both as direct vehicle columns and as dynamic attribute concepts:

| Concept | Legacy vehicle column | Dynamic attribute equivalent |
| --- | --- | --- |
| Body Type | `vehicle_type` | `Body Type` |
| Fuel Type | `fuel_type` | `Fuel Type` |
| Transmission | `transmission` | `Transmission` |
| Color | `color` | `Color` |
| Engine Capacity | `engine_capacity` | `Engine CC` |

Additional likely duplication risk:

- `doors` and `seats` may remain as hard columns while similar specs could also be represented dynamically depending on catalog design
- `location` / `stock_country` are hard columns and not part of the dynamic attribute catalog

### Features vs Attributes

Some real-world vehicle characteristics can be represented in two conceptually overlapping systems:

- Dynamic attributes for typed catalog specs
- `features_json` for boolean conveniences and equipment flags

Possible overlap examples:

- Safety/equipment items like ABS, airbags, ESC, camera features
- Some of these could be model-driven attributes in a catalog system, but they are currently feature flags

### Make/Model Duplication

The system stores make/model twice:

- Normalized: `make_id`, `model_id`
- Legacy strings: `make`, `model`

This duplication is deliberate for backward compatibility and search/display continuity.

## 8. Current Problems

### 1. Duplicated Data Paths

The biggest structural issue is duplication between:

- Legacy columns on `vehicles`
- Dynamic attributes in `vehicle_attribute_values`
- Feature booleans in `features_json`

This increases:

- Form complexity
- Validation complexity
- Risk of stale or conflicting values

### 2. Admin Form Complexity

The shared vehicle form has historically taken on too many responsibilities:

- Make/model dependency management
- Model code loading
- Dynamic attribute rendering
- Legacy field editing
- Feature editing
- Image uploads
- Validation display for both top-level and nested fields

This made admin edit flows fragile, especially around initialization and validation sequencing.

### 3. Validation Conflicts Around Dynamic Attributes

Previous failures were caused by the gap between:

- frontend sending empty/null dynamic attribute rows
- backend treating model attributes as required or type-valid even when no value was intended

That led to:

- `dynamic_attributes.X is required`
- Inertia validation redirects that surfaced as 303 responses
- admin edit pages appearing broken even though the real issue was request validation

### 4. Compatibility Burden

Customer-facing pages still read legacy fields such as:

- `vehicle_type`
- `color`
- `transmission`
- `fuel_type`
- `engine_capacity`

So admin-side simplification cannot safely remove those columns or rename them. The Vehicle model compatibility layer is doing essential work to keep those flows alive.

### 5. Performance Considerations

Potential performance pressure points:

- Public listing eager-loads `attributeValues.attribute` and `attributeValues.value`
- Dynamic faceting joins `vehicle_attribute_values` repeatedly in `VehicleFilterService`
- Attribute merging uses service-layer transformations and per-model metadata
- Search/order pages often load full vehicle relationships repeatedly

The existing indexes help, but the module is still non-trivial and could become expensive as catalog size grows.

### 6. Mixed Source of Truth

The module does not have a single source of truth for all specifications:

- Some specs are effectively owned by dynamic attributes
- Some remain in hard columns
- Some are mirrored from dynamic attributes back into legacy columns
- Some boolean capabilities live in `features_json`

This is the core reason refactors need to be conservative.

## 9. Current Safe Conclusions

Based on the current codebase, the safe interpretation of the module is:

- Public and customer flows still depend on the legacy compatibility layer
- Dynamic attributes are the flexible catalog layer, but not yet the only source of truth everywhere
- `features_json` is a separate lightweight equipment system, not a replacement for typed specs
- Admin create/edit can be simplified safely only if compatibility columns and accessors remain untouched
- Any future refactor should target admin-side UX and request normalization first, not schema removal

## 10. Key Files Reference

### Schema / Migrations

- `database/migrations/2026_04_07_195624_create_vehicles_table.php`
- `database/migrations/2026_04_07_222633_add_extra_fields_to_vehicles_table.php`
- `database/migrations/2026_04_08_170327_add_discount_fields_to_vehicles_table.php`
- `database/migrations/2026_04_08_171020_add_type_and_stock_country_to_vehicles_table.php`
- `database/migrations/2026_04_09_000001_add_status_and_features_to_vehicles_table.php`
- `database/migrations/2026_04_10_000002_create_vehicle_catalog_tables.php`
- `database/migrations/2026_04_10_000006_create_make_attributes_and_attribute_defaults.php`
- `database/migrations/2026_04_10_000007_create_model_attribute_defaults_table.php`
- `database/migrations/2026_04_10_000009_extend_model_attributes_for_admin_mapping.php`
- `database/migrations/2026_04_15_000007_add_model_code_to_vehicles_table.php`

### Models / Core Logic

- `app/Models/Vehicle.php`
- `app/Models/Make.php`
- `app/Models/VehicleModel.php`
- `app/Models/VehicleAttribute.php`
- `app/Models/VehicleAttributeValue.php`

### Services

- `app/Services/VehicleFilterService.php`
- `app/Services/VehicleCatalogAttributeService.php`
- `app/Services/VehicleFormOptionsService.php`

### Admin / Seller Create-Edit

- `app/Http/Controllers/Admin/VehicleController.php`
- `app/Http/Controllers/Seller/VehicleController.php`
- `app/Http/Requests/StoreVehicleRequest.php`
- `app/Http/Requests/UpdateVehicleRequest.php`
- `resources/js/Components/Vehicles/VehicleForm.vue`
- `resources/js/Pages/Admin/Vehicles/Create.vue`
- `resources/js/Pages/Vehicles/Edit.vue`
- `resources/js/Pages/Seller/Vehicles/Create.vue`
- `resources/js/Pages/Seller/Vehicles/Edit.vue`

### Public / Customer Usage

- `app/Http/Controllers/PublicController.php`
- `resources/js/Pages/Public/Home.vue`
- `resources/js/Pages/Public/VehicleShow.vue`
- `app/Http/Controllers/Customer/CheckoutController.php`
- `app/Http/Controllers/Customer/OrderController.php`

## Summary

The Vehicle module is currently a backward-compatible hybrid system.

- The `vehicles` table remains the main record store.
- The normalized make/model catalog and dynamic attributes system provide structure and filtering power.
- Legacy columns are still actively required by public display and purchase flows.
- `features_json` is a separate boolean equipment system.
- The main technical risks are duplicated specification storage, complex form initialization, and validation conflicts between optional frontend state and strict backend assumptions.

Any future refactor should treat customer-facing compatibility as a hard constraint and keep schema/API behavior stable while simplifying admin workflows incrementally.