# esx_vehicleshop (/docs/addons/esx_vehicleshop)

esx_vehicleshop is the official car dealership for ESX Legacy. It turns Premium Deluxe Motorsport into a showroom where players buy vehicles that are saved to `owned_vehicles`, and optionally into a player-run `cardealer` job that orders stock, sells or rents cars to other players, and gets paid daily rent.

## Overview [#overview]

| | |
| --- | --- |
| Resource | `esx_vehicleshop` (version `1.0`) |
| Side | Client + server |
| Dependencies | `esx_lib`, `es_extended`, `oxmysql` |
| Optional | `esx_society`, `esx_addonaccount`, `esx_addoninventory` (job mode), `esx_license` (license check), `cron` (rent) |
| SQL file | `esx_vehicleshop.sql` |
| Client export | `GeneratePlate` |

Two modes, controlled by `Config.EnablePlayerManagement`:

- **Direct sales (`false`, default).** Anyone can enter the showroom, browse `vehicles` by category and buy with **cash**. The vehicle spawns outside, the player is warped in, and a row is added to `owned_vehicles` with a server-generated plate.
- **Car dealer job (`true`).** Players with the `cardealer` job buy stock with society money into `cardealer_vehicles`, then sell or rent it to nearby players, return it to the provider for 75 % of the price, and manage the society stash through the boss actions marker. The society `cardealer` is registered with [esx_society](/docs/addons/esx_society).

Reselling at the `ResellVehicle` marker pays `Config.ResellPercentage` of the catalog price and deletes the vehicle from `owned_vehicles`.

## Installation [#installation]

<Steps>
<Step>
### Import the SQL [#import-the-sql]

`esx_vehicleshop.sql` creates the tables `cardealer_vehicles`, `vehicle_sold`, `owned_vehicles`, `rented_vehicles`, `vehicle_categories` and `vehicles` (with the default catalog), plus the `cardealer` job, its grades and the `society_cardealer` addon account and inventory.

If `owned_vehicles` already exists (for example from another garage script), remove that `CREATE TABLE` from the file before importing. All of `cardealer_vehicles`, `owned_vehicles`, `rented_vehicles` and `vehicle_sold` must use InnoDB, otherwise dealer transactions are disabled at startup.
</Step>
<Step>
### Ensure the resource [#ensure-the-resource]

```bash title="server.cfg"
ensure oxmysql
ensure esx_lib
ensure es_extended
ensure cron
ensure esx_addonaccount
ensure esx_addoninventory
ensure esx_society
ensure esx_license
ensure esx_vehicleshop
```

Only the first three are required in direct sales mode.
</Step>
</Steps>

On first start the resource also creates `vehicle_rental_payments` (daily rent receipts), converts a legacy `varchar` `vehicle_sold.date` to `DATETIME` when all values are valid, and adds the `idx_vehicle_sold_date_plate` index.

## Configuration [#configuration]

| Key | Default | Description |
| --- | --- | --- |
| `Config.DrawDistance` | `10` | Marker draw distance. |
| `Config.MarkerColor` | `{r = 120, g = 120, b = 240}` | Marker color. |
| `Config.EnablePlayerManagement` | `false` | Enables the `cardealer` job mode. Needs esx_addonaccount, esx_billing and esx_society. |
| `Config.ResellPercentage` | `50` | Percent of the catalog price paid when reselling. |
| `Config.RentalPriceMultiplier` | `0.05` | Daily rent = base price × multiplier. |
| `Config.MinimumRentalPrice` | `1` | Lowest daily rent. |
| `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale. |
| `Config.LicenseEnable` | `false` | Require the `drive` license before opening the shop menu (direct mode, client-side check). |
| `Config.PlateLetters` | `3` | Letters in generated plates. |
| `Config.PlateNumbers` | `3` | Digits in generated plates. |
| `Config.PlateUseSpace` | `true` | Space between letters and digits. Letters + numbers + space must not exceed 8. |
| `Config.OxInventory` | `ESX.GetConfig().OxInventory` | Uses an ox_inventory stash for the society stock. |
| `Config.Blip` | `{ show = true, Sprite = 326, Display = 4, Scale = 0.8 }` | Dealership blip. |

### Zones [#zones]

`Config.Zones` defines six points. `Type = -1` hides the marker.

| Zone | Default position | Purpose |
| --- | --- | --- |
| `ShopEntering` | `vector3(-33.7, -1102.0, 25.4)` | Opens the shop / dealer menu. |
| `ShopInside` | `vector3(-47.5, -1097.2, 25.4)` | Preview spot. Server checks for sales use 30 m around it. |
| `ShopOutside` | `vector3(-28.6, -1085.6, 25.5)` | Spawn point of bought vehicles. |
| `BossActions` | `vector3(-32.0, -1114.2, 25.4)` | Boss menu and stock (job mode). |
| `GiveBackVehicle` | `vector3(-18.2, -1078.5, 25.6)` | Return a rented vehicle. |
| `ResellVehicle` | `vector3(-44.6, -1080.7, 25.6)` | Sell an owned vehicle back. |

### Catalog tables [#catalog-tables]

```sql
INSERT INTO `vehicle_categories` (`name`, `label`) VALUES ('electric', 'Electric');
INSERT INTO `vehicles` (`name`, `model`, `price`, `category`) VALUES ('Neon', 'neon', 150000, 'electric');
```

Vehicles whose `category` has no matching row in `vehicle_categories` are not shown (the query uses an inner join).

## Rentals [#rentals]

In job mode a dealer can rent a stock car to a player. The rent is computed on the server (`getRentalPrice`). Every day at 22:00 the `cron` resource triggers `PayRent()`: rent is taken from each renter's **bank** and added to `society_cardealer`. If a renter cannot pay, the rental is removed ("evicted"). Each charge is written to `vehicle_rental_payments`, so a restart does not charge twice on the same day.

## Developer API [#developer-api]

### Client export `GeneratePlate` [#client-export-generateplate]

```lua
local plate = exports.esx_vehicleshop:GeneratePlate()
```

Returns a random plate in the configured format that is not in `owned_vehicles` (checked with the `esx_vehicleshop:isPlateTaken` callback). It yields, so call it from a thread. The shop itself generates plates on the server and ignores client plates.

```lua title="client/main.lua"
CreateThread(function()
    local plate = exports.esx_vehicleshop:GeneratePlate()
    print(('Free plate: %s'):format(plate))
end)
```

### Server callbacks [#server-callbacks]

| Callback | Arguments | Who | Notes |
| --- | --- | --- | --- |
| `esx_vehicleshop:buyVehicle` | `model`, `plate` | Anyone within 30 m of `ShopInside` | Plate argument is ignored, price comes from `vehicles`. |
| `esx_vehicleshop:resellVehicle` | `plate`, `model` (hash) | Owner near `ResellVehicle` | Checks ownership, model and plate. |
| `esx_vehicleshop:giveBackVehicle` | `plate` | Renter near `GiveBackVehicle` | Vehicle with that plate must be within 8 m. |
| `esx_vehicleshop:buyCarDealerVehicle` | `model` | `cardealer` near showroom | Pays from `society_cardealer`. |
| `esx_vehicleshop:getCommercialVehicles` | none | `cardealer` | Current stock. |
| `esx_vehicleshop:getRentedVehicles` | none | `cardealer` near boss marker | Active rentals. |
| `esx_vehicleshop:getSoldVehicles` | `page` | `cardealer` | Sales log, 50 per page. |
| `esx_vehicleshop:getStockItems` / `getPlayerInventory` | none | `cardealer` / player | Society stash and own inventory. |
| `esx_vehicleshop:isPlateTaken` | `plate` | Anyone | `true` if the plate exists in `owned_vehicles`. |
| `esx_vehicleshop:retrieveJobVehicles` | `type` | Player | Own vehicles of `type` for the current job. |

### Net events (client to server) [#net-events-client-to-server]

| Event | Arguments | Guard |
| --- | --- | --- |
| `esx_vehicleshop:setVehicleOwnedPlayerId` | `playerId, vehicleProps, model, label` | `cardealer` near showroom, model must be in stock. Sells a stock car to `playerId`. |
| `esx_vehicleshop:rentVehicle` | `vehicle, plate, rentPrice, playerId` | Same guard. `plate` and `rentPrice` are ignored, the server computes both. |
| `esx_vehicleshop:returnProvider` | `vehicleModel` | `cardealer` near boss or resell marker. |
| `esx_vehicleshop:getStockItem` / `putStockItems` | `itemName, count` | `cardealer` near boss or resell marker. |
| `esx_vehicleshop:setJobVehicleState` | `plate, state` | Only sets `stored = 0` for the player's own job vehicle near them. |
| `esx_vehicleshop:getVehiclesAndCategories` | none | Sends the catalog to the caller. |

The server broadcasts `esx_vehicleshop:updateVehiclesAndCategories(vehicles, categories, vehiclesByModel)` to clients.

<Callout type="warn" title="Security notes">
All money-moving events and callbacks are guarded by job, distance and database transactions. Two gaps remain: the `drive` license check (`Config.LicenseEnable`) runs only on the client, and `isPlateTaken` can be called by anyone. If the license must be enforced, add a server-side `esx_license:checkLicense` call to your fork of `buyVehicle`.
</Callout>

## Common issues [#common-issues]

- **"Dealer transactions disabled" in the console.** One of the dealer tables is MyISAM. Convert it with `ALTER TABLE <name> ENGINE=InnoDB;`.
- **Shop menu is empty.** The `vehicles` table is empty or categories do not match `vehicle_categories.name`.
- **Plate length warning.** `PlateLetters + PlateNumbers` (+1 with space) is above 8.
- **Rent is never charged.** The `cron` resource is not started, or `society_cardealer` is missing in `addon_account`.
- **Job menu does not show.** `Config.EnablePlayerManagement` is off or the player does not have the `cardealer` job.

## Related [#related]

- [esx_garage](/docs/addons/esx_garage)
- [esx_society](/docs/addons/esx_society)
- [esx_license](/docs/addons/esx_license)
- [cron](/docs/core/cron)
- [Database schema](/docs/guides/database-schema)
