Skip to Content
Resourcesqb-vehiclesales Reference

qb-vehiclesales

qb-vehiclesales is the player-to-player used car lot. An owner parks a vehicle they own, lists it at a price, and another player buys it. Ownership moves between player_vehicles rows and the seller receives the sale price minus a commission.

It is distinct from qb-vehicleshop, which sells new vehicles from a dealership catalogue, and from qb-garages, which only stores vehicles a player already owns.

Note the internal namespace: the resource folder is qb-vehiclesales, but almost every event is qb-occasions:. Only one callback uses the qb-vehiclesales: prefix.

Before deploying it, read The listing price comes from the client.

The manifest at the commit linked above declares version 1.5.0.

Dependencies and start order

From the manifest:

  • Shared: config.lua, @qb-core/shared/locale.lua, locales
  • Client: @PolyZone/client.lua, BoxZone, EntityZone, CircleZone, ComboZone, client.lua
  • Server: @oxmysql/lib/MySQL.lua, server.lua
  • ui_page 'html/ui.html', with a bundled vue.min.js

Start order: oxmysql, qb-core, PolyZone, then qb-vehiclesales. Your garage resource should also be started, because ownership rows written here are what garages read.

Database

The repository ships qb-vehiclesales.sql:

CREATE TABLE IF NOT EXISTS `occasion_vehicles` ( `id` int(11) NOT NULL AUTO_INCREMENT, `seller` varchar(50) DEFAULT NULL, `price` int(11) DEFAULT NULL, `description` longtext DEFAULT NULL, `plate` varchar(50) DEFAULT NULL, `model` varchar(50) DEFAULT NULL, `mods` text DEFAULT NULL, `occasionid` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`), KEY `occasionId` (`occasionid`) ) ENGINE=InnoDB AUTO_INCREMENT=325 DEFAULT CHARSET=utf8mb4;

seller holds a citizenid. occasionid is a generated listing ID used together with the plate to address a listing.

The resource also reads and writes two tables it does not own:

  • player_vehicles, for the actual ownership rows. Listing deletes the owner’s row, buying inserts a new one for the buyer, delisting reinserts it for the seller.
  • players, to pay an offline seller by rewriting their money JSON column directly.

Back up both before first deployment. A failure between the delete and the insert loses a vehicle.

Configuration

config.lua is short at this commit.

KeyDefaultMeaning
Config.UseTargetGetConvar('UseTarget', 'false') == 'true'Set setr UseTarget true in server.cfg
Config.ZonestableLot zones, display spots, and interaction points

There is no configurable commission, price ceiling, or listing limit. Those are hardcoded, see below.

Server surface

HandlerKindArguments
qb-occasions:server:getVehiclescallbacknone, returns all listings
qb-occasions:server:checkVehicleOwnercallbackplate
qb-occasions:server:getSellerInformationcallbackcitizenid
qb-vehiclesales:server:CheckModelNamecallbackplate
qb-occasions:server:sellVehiclenet eventvehiclePrice, vehicleData
qb-occasions:server:buyVehiclenet eventvehicleData
qb-occasions:server:ReturnVehiclenet eventvehicleData
qb-occasions:server:sellVehicleBacknet eventvehData

Ownership checks

Listing and selling back both prove ownership through the database rather than through a client claim, which is the right shape:

local ownsVehicle = MySQL.query.await( 'DELETE FROM player_vehicles WHERE plate = ? AND citizenid = ? and vehicle = ?', { vehicleData.plate, Player.PlayerData.citizenid, vehicleData.model } ) if ownsVehicle.affectedRows > 0 then -- the row existed and belonged to this citizenid, proceed end

ReturnVehicle similarly checks that result[1].seller equals the caller’s citizenid before handing the vehicle back.

The listing price comes from the client

qb-occasions:server:sellVehicle takes vehiclePrice as its first argument and writes it straight into occasion_vehicles.price. At this commit there is no minimum, no maximum, no type check, and no comparison against the vehicle’s shared price.

That is fine as long as the seller is the one who sets it, which is the intended flow. It becomes a problem when combined with the buy path, because a listing price is also what the buyer is charged and what the seller is paid. A modified client can list at an arbitrary value.

If you accept player-set prices at all, clamp them server side before this reaches the database:

-- in your own wrapper, or patched into server.lua local MIN_PRICE, MAX_PRICE = 1, 5000000 local price = math.floor(tonumber(vehiclePrice) or 0) if price < MIN_PRICE or price > MAX_PRICE then return end

Treat every client argument the same way. See Server functions for the core helpers available on the server side, and the server-authority checklist for the review questions.

Buying and commission

buyVehicle requires the buyer’s bank balance to cover result[1].price, removes that amount, inserts a player_vehicles row for the buyer, and pays the seller math.ceil((price / 100) * 77). The 23 percent difference is a hardcoded commission. It is not configurable at this commit.

If the seller is online the payment goes through SellerData.Functions.AddMoney('bank', ...). If they are offline, the resource reads the players row, decodes the money JSON, adjusts it, and writes it back. That path bypasses the normal money API, so it will not appear in any transaction log your core keeps.

Selling back to the lot

sellVehicleBack pays 50 percent of the vehicle’s shared price, looked up from the shared vehicle list by hash, and deletes the player_vehicles row. A vehicle whose model is not in your shared vehicles list resolves to a price of 0 and the player receives nothing while still losing the row.

Troubleshooting

SymptomLikely cause
”Not your vehicle” for a vehicle the player ownsThe player_vehicles row’s vehicle or hash column does not match what the client sent. sellVehicle matches on vehicle, sellVehicleBack matches on hash
Vehicle vanished after listingThe delete succeeded and the insert into occasion_vehicles failed. Check the oxmysql log
Seller never paidThe seller was offline and the players row rewrite failed, or the seller’s citizenid no longer exists
Selling back pays nothingThe model is missing from your shared vehicles list, so the lookup returns 0
Listings do not appearoccasion_vehicles was not created, or the callback errored. Check the server console
Absurd listing pricesThe price is accepted from the client unvalidated. Add a clamp

Proposed manual smoke test

Not yet executed. On staging, with a database backup:

  1. Confirm occasion_vehicles exists before starting the resource.
  2. List a vehicle, confirm the player_vehicles row is gone and the occasion_vehicles row exists.
  3. Buy it as a second character and confirm both balances and the new ownership row.
  4. Confirm the seller receives 77 percent, and repeat with the seller offline.
  5. Delist a vehicle and confirm ownership returns.
  6. Sell one back to the lot and confirm the 50 percent payout.

Sources