> For the complete documentation index, see [llms.txt](https://atiysus-organization.gitbook.io/aty-scripts/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://atiysus-organization.gitbook.io/aty-scripts/garage-v2/garage-v2.md).

# Garage V2

## aty\_garage\_v2

Full-featured multi-framework garage system: public/private/manageable/job/impound/lost-vehicle garages, pre-built IPL interior garages, park-in-place persistence, vehicle insurance, key sharing, shared-garage members, `ps-housing` house garages, and a police fine/impound workflow — all driven by a Vue NUI and the `aty_lib` framework bridge.

### Info

|               |                                                                                         |
| ------------- | --------------------------------------------------------------------------------------- |
| Resource name | `aty_garage_v2`                                                                         |
| Frameworks    | ESX / QBCore / Qbox (via `qbx_core` qb-core compat bridge) — detected through `aty_lib` |
| Database      | MySQL (oxmysql/mysql-async, via `aty_lib` → `modules.ExecuteSql`)                       |

### Dependencies

| Dependency                                          | Required                           | Notes                                                                                                                                                                                    |
| --------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aty_lib`                                           | Yes (declared in `fxmanifest.lua`) | Framework bridge: `getModules()`, `getUtils()`, `getShared()` — SQL helper, callbacks (`modules.RegisterCallback`/`TriggerCallback`), notifications, identifiers, player job/gang lookup |
| `qb-core` / `es_extended` / `qbx_core`              | One of these                       | Framework detected via `Utils.Framework` (`'qb-core'` or `'es_extended'`); Qbox works through its qb-core compat layer                                                                   |
| `ox_target` or `qb-target`                          | Optional                           | Only needed if `Config.Interaction = 'target'`. Auto-detected; falls back to marker mode if neither is running                                                                           |
| `ps-housing`                                        | Optional                           | Only needed if `Config.Housing.enabled = true` — reads its `properties` table for house garages                                                                                          |
| `okokBilling` / `esx_billing` / qb `phone_invoices` | Optional                           | Used by the police billing bridge (`server/billing.lua`) when `Config.Police.billing = 'auto'` (or a specific mode)                                                                      |

### Installation

1. Copy the `aty_garage_v2` folder into your resources (e.g. `[aty]/aty_garage_v2`).
2. No manual SQL import is required — on first start the resource runs `CREATE TABLE IF NOT EXISTS` statements itself (in `server/main.lua`) for all of its tables: `aty_garages`, `aty_garage_ipls`, `aty_garage_vehmeta`, `aty_garage_members`, `aty_garage_keys`, `aty_garage_parked`, `aty_garage_fines`, `aty_garage_impounds`. It also auto-migrates a few legacy columns (e.g. adds `gang`/`gang_grade` to `aty_garages`, `stored_at`/`insurance_until`/`destroyed` to `aty_garage_vehmeta` if missing). It reads/writes vehicle ownership from your framework's existing table: `player_vehicles` (QBCore) or `owned_vehicles` (ESX).
3. Add to `server.cfg`, making sure `aty_lib` starts first:

   ```cfg
   ensure aty_lib
   ensure aty_garage_v2
   ```
4. Configure `shared/config.lua` (client + server visible) and `server/config.lua` (server-only — Discord webhook settings; never sent to clients).
5. If using the pre-built IPL garages, verify/adjust the interior coordinates for your server with the `/iploc` and `/iplhere` diagnostic commands (see Commands), then place the actual menu/preview/spawn/zone points per IPL with `/garageipl`.
6. Create garages in-game with `/garageadmin` (or the command configured in `Config.AdminCommand`).

### Configuration

#### `shared/config.lua` — General

| Option     | Type    | Default | Description                                                                                                               |
| ---------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
| `Debug`    | boolean | `false` | Debug flag (used sparingly in code)                                                                                       |
| `Language` | string  | `'en'`  | Active locale (`'en'` \| `'tr'`), loaded from `locales/<lang>.lua`; missing keys fall back to EN via `locales/loader.lua` |

#### Interaction

| Option                | Type   | Default                                | Description                                                                                                                                          |
| --------------------- | ------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Interaction`         | string | `'target'`                             | `'marker'` (3D marker + key prompt) or `'target'` (ox\_target/qb-target zones, auto-detected; falls back to marker if no target resource is running) |
| `TargetIcons`         | table  | `{ open, enter, store, buy, impound }` | FontAwesome icon per target action (keys: `open`, `enter`, `store`, `buy`, `impound`)                                                                |
| `InteractionKey`      | number | `38` (E)                               | Key to open/interact in marker mode                                                                                                                  |
| `InteractionDistance` | number | `3.0`                                  | Distance to open a garage menu / enter an IPL                                                                                                        |
| `StoreDistance`       | number | `8.0`                                  | Distance (in a vehicle) to store/park                                                                                                                |
| `MarkerDrawDistance`  | number | `15.0`                                 | Distance at which world markers are drawn                                                                                                            |

#### Admin

| Option               | Type         | Default                                           | Description                                                                                       |
| -------------------- | ------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `AdminCommand`       | string       | `'garageadmin'`                                   | Command that opens the garage creation/admin menu                                                 |
| `IplCommand`         | string       | `'garageipl'`                                     | Command that opens the IPL interior editor directly                                               |
| `QBPermission`       | string/table | `{ 'admin', 'god' }`                              | QBCore permission level(s) for `AdminCommand`/`IplCommand` (registered via `QBCore.Commands.Add`) |
| `ESXGroup`           | string/table | `{ 'admin', 'superadmin' }`                       | ESX group(s) for the same commands (registered via `ESX.RegisterCommand`)                         |
| `AdminAce`           | string       | `'aty_garage.admin'`                              | Extra ace permission checked as a defense-in-depth layer on admin callbacks                       |
| `AdminGroups`        | table        | `{ admin = true, god = true, superadmin = true }` | Framework group names accepted by the internal `isAdmin()` check                                  |
| `PlaceholderVehicle` | string       | `'adder'`                                         | Model shown while placing IPL "Vehicle Spawn" points in the editor                                |

#### Vehicles & economy

| Option                    | Type    | Default  | Description                                                                                      |
| ------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------ |
| `SharedGarages`           | boolean | `false`  | If `true`, a vehicle can be taken out directly from any garage regardless of where it was stored |
| `Warp`                    | boolean | `true`   | Auto-warp the player into the vehicle after spawn                                                |
| `VisuallyDamageCars`      | boolean | `true`   | Re-apply visual damage on spawn                                                                  |
| `DefaultCapacity`         | number  | `25`     | Default garage vehicle capacity                                                                  |
| `Bring.enabled`           | boolean | `true`   | Enable "bring vehicle from another garage" (used when `SharedGarages = false`)                   |
| `Bring.price`             | number  | `1500`   | Fee charged to bring a vehicle                                                                   |
| `Bring.account`           | string  | `'bank'` | Account charged for `Bring`                                                                      |
| `Owned.account`           | string  | `'bank'` | Account used for private garage purchase/sale                                                    |
| `Owned.maxHourlyFee`      | number  | `100`    | Cap on the hourly fee an owner can set on their private garage                                   |
| `Owned.sellRefundPercent` | number  | `50`     | Refund percentage when a private garage is sold                                                  |
| `JobPlatePrefix`          | string  | `'JOB'`  | Plate prefix used for job fleet vehicles                                                         |
| `Lost.retrieveFee`        | number  | `2500`   | Fee to retrieve a "lost" vehicle (not impounded, not destroyed) into a normal garage             |
| `Lost.account`            | string  | `'bank'` | Account charged for `Lost` retrieval                                                             |

#### Integration hooks

| Option                          | Type            | Default | Description                                                                                 |
| ------------------------------- | --------------- | ------- | ------------------------------------------------------------------------------------------- |
| `Integrations.getFuel`          | function \| nil | `nil`   | Override fuel read (falls back to `aty_lib` bridge default); errors are caught with `pcall` |
| `Integrations.setFuel`          | function \| nil | `nil`   | Override fuel write                                                                         |
| `Integrations.onVehicleSpawned` | function \| nil | `nil`   | Called `(veh, plate, props)` after a vehicle is spawned from a garage                       |
| `Integrations.onVehicleStored`  | function \| nil | `nil`   | Called `(veh, plate, props)` right before a stored vehicle's entity is deleted              |

#### Event hooks (`Config.Hooks`, client-side)

Every hook is optional (`nil` = disabled) and pcall-protected. Each one **also** fires a Lua event `aty_garage_v2:hook:<name>` with the same arguments, so other resources can listen with `AddEventHandler` without editing the config.

| Hook                | Arguments              | Fired when                                                                                                                      |
| ------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `onMenuOpen`        | `(garage)`             | The garage menu opens                                                                                                           |
| `onMenuClose`       | `(garage)`             | The garage menu closes                                                                                                          |
| `onIplEnter`        | `(garage)`             | The player enters an IPL interior                                                                                               |
| `onIplExit`         | `(garage)`             | The player exits an IPL interior                                                                                                |
| `onLockpickStart`   | `(veh, plate)`         | A lockpick attempt starts (after server validation)                                                                             |
| `onLockpickSuccess` | `(veh, plate)`         | The lock is picked successfully                                                                                                 |
| `onLockpickFail`    | `(veh, plate, broke)`  | The attempt fails (`broke` = lockpick item broke)                                                                               |
| `onLockpickAlert`   | `(coords, plate, veh)` | Police alert roll passes (`Lockpick.alertChance`) — wire to your dispatch script, e.g. `exports['ps-dispatch']:CarJacking(veh)` |
| `onVehicleTaken`    | `(veh, plate, props)`  | A vehicle is taken out and spawned                                                                                              |
| `onVehicleStored`   | `(veh, plate, props)`  | A vehicle is stored (entity still exists)                                                                                       |

Example — dispatch alert without touching the config, from any resource:

```lua
AddEventHandler('aty_garage_v2:hook:onLockpickAlert', function(coords, plate, veh)
    exports['ps-dispatch']:CarJacking(veh)
end)
```

#### Housing (`ps-housing`)

| Option                   | Type        | Default        | Description                                                                         |
| ------------------------ | ----------- | -------------- | ----------------------------------------------------------------------------------- |
| `Housing.enabled`        | boolean     | `true`         | Enable dynamic house garages generated from `ps-housing`'s `properties.garage_data` |
| `Housing.script`         | string      | `'ps-housing'` | Only `'ps-housing'` is currently supported                                          |
| `Housing.capacity`       | number      | `10`           | Capacity of each house garage                                                       |
| `Housing.rescanInterval` | number (ms) | `300000`       | Periodic re-scan interval of the `properties` table                                 |
| `Housing.blip`           | boolean     | `false`        | Whether to show a map blip for house garages                                        |

#### Insurance & vehicle recovery

| Option                          | Type    | Default  | Description                                                                        |
| ------------------------------- | ------- | -------- | ---------------------------------------------------------------------------------- |
| `Insurance.enabled`             | boolean | `true`   | Enable per-vehicle insurance purchase                                              |
| `Insurance.account`             | string  | `'bank'` | Account charged for insurance                                                      |
| `Insurance.price`               | number  | `5000`   | Policy price                                                                       |
| `Insurance.durationDays`        | number  | `7`      | Policy duration in days (re-purchase extends it)                                   |
| `Insurance.claimFee`            | number  | `1000`   | Fee to recover an insured, destroyed vehicle                                       |
| `Insurance.allowUninsured`      | boolean | `true`   | Whether an uninsured destroyed vehicle can still be recovered (for `uninsuredFee`) |
| `Insurance.uninsuredFee`        | number  | `15000`  | Fee to recover an uninsured destroyed vehicle                                      |
| `Insurance.cancelRefundPercent` | number  | `50`     | Refund percentage (of remaining policy value) on cancellation                      |

#### Preview camera

| Option              | Type    | Default               | Description                                          |
| ------------------- | ------- | --------------------- | ---------------------------------------------------- |
| `Preview.enabled`   | boolean | `true`                | Enable the vehicle preview camera in garage menus    |
| `Preview.dof`       | boolean | `false`               | Depth-of-field (blur background, keep vehicle sharp) |
| `Preview.camOffset` | vector3 | `vec3(4.2, 3.6, 1.5)` | Camera offset from the previewed vehicle             |
| `Preview.fov`       | number  | `45.0`                | Camera field of view                                 |

#### Blips

| Option               | Type    | Default         | Description                                                                                  |
| -------------------- | ------- | --------------- | -------------------------------------------------------------------------------------------- |
| `Blip.show`          | boolean | `true`          | Show garage blips on the map                                                                 |
| `Blip.sprite`        | number  | `357`           | Blip sprite ID                                                                               |
| `Blip.color`         | number  | `3`             | Blip color ID                                                                                |
| `Blip.size`          | number  | `0.8`           | Blip scale                                                                                   |
| `Blip.toggleCommand` | string  | `'garageblips'` | Client command to toggle blip visibility per player (preference persisted in a resource KVP) |

#### Blur (menu background)

| Option             | Type        | Default | Description                                                                                                 |
| ------------------ | ----------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| `Blur.enabled`     | boolean     | `true`  | Enable frosted-glass background blur in the NUI (captured via screenshot-basic + GLSL, not the native blur) |
| `Blur.interval`    | number (ms) | `350`   | Recapture interval — lower = livelier but costs more performance                                            |
| `Blur.settleDelay` | number (ms) | `250`   | Delay before the first capture after the menu opens (lets the camera settle)                                |

#### Mileage

| Option                 | Type        | Default | Description                                |
| ---------------------- | ----------- | ------- | ------------------------------------------ |
| `Mileage.enabled`      | boolean     | `true`  | Enable client→server mileage tracking/sync |
| `Mileage.syncInterval` | number (ms) | `60000` | Client sync interval                       |

#### Park-in-place

| Option                       | Type        | Default | Description                                                                         |
| ---------------------------- | ----------- | ------- | ----------------------------------------------------------------------------------- |
| `ParkInPlace.enabled`        | boolean     | `true`  | Store vehicles in the world at the spot they were parked instead of despawning them |
| `ParkInPlace.parkRadius`     | number      | `30.0`  | Max distance from the garage center a vehicle may be parked                         |
| `ParkInPlace.spawnDistance`  | number      | `60.0`  | Distance at which a nearby player triggers the entity to (re)spawn                  |
| `ParkInPlace.deleteDistance` | number      | `85.0`  | Distance beyond which the entity is deleted (all players out of range)              |
| `ParkInPlace.checkInterval`  | number (ms) | `4000`  | Server-side distance check interval                                                 |

#### Police / fines / impound

| Option                              | Type            | Default                                                                                                       | Description                                                                                                                                                      |
| ----------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Police.jobs`                       | table           | `{ police = true, sheriff = true }`                                                                           | Jobs allowed to use fine/impound features                                                                                                                        |
| `Police.fineKey`                    | number          | `47` (G)                                                                                                      | Key to open the fine/impound prompt near a parked vehicle                                                                                                        |
| `Police.interactDistance`           | number          | `4.0`                                                                                                         | Distance for the fine/impound prompt                                                                                                                             |
| `Police.minFine` / `Police.maxFine` | number          | `50` / `10000`                                                                                                | Allowed fine amount range                                                                                                                                        |
| `Police.payAccount`                 | string          | `'bank'`                                                                                                      | Internal collection account (used when no external billing succeeds)                                                                                             |
| `Police.reasons`                    | table           | 4 presets (`Illegal Parking` 250, `Blocking Traffic` 400, `Abandoned Vehicle` 600, `Parking on Sidewalk` 300) | Preset reasons shown in the fine/impound menu (amount editable)                                                                                                  |
| `Police.impound.searchRadius`       | number          | `18.0`                                                                                                        | Radius to find a target vehicle for impound                                                                                                                      |
| `Police.impound.defaultEarlyFee`    | number          | `1500`                                                                                                        | Fee to release before the set release date                                                                                                                       |
| `Police.impound.defaultLateFee`     | number          | `1000`                                                                                                        | Fee to release after the set release date (normal)                                                                                                               |
| `Police.impound.minFee` / `maxFee`  | number          | `0` / `100000`                                                                                                | Allowed impound fee range                                                                                                                                        |
| `Police.impound.maxDays`            | number          | `30`                                                                                                          | Max days in the future for the release date                                                                                                                      |
| `Police.billing`                    | string          | `'auto'`                                                                                                      | `'auto'` (tries okokBilling → esx\_billing → qb `phone_invoices` → internal), or force one of `'okokbilling'`, `'esx_billing'`, `'qb'`, `'internal'`, `'custom'` |
| `Police.society`                    | table           | `{ name = 'police', account = 'society_police', label = 'LSPD' }`                                             | Society info passed to the billing bridge                                                                                                                        |
| `Police.customBill`                 | function \| nil | `nil`                                                                                                         | Custom billing hook used when `Police.billing = 'custom'`; return `true` if billed externally                                                                    |

#### IPL interiors — fully in-game, DB-backed

IPL interiors are no longer defined in config (`shared/ipls.lua` was removed). They live entirely in the `aty_garage_ipls` table and are created/edited in-game via the IPL editor (`/garageipl` → dropdown → **+ Create New Interior**). Each record holds its own IPL names to load (`ipl_names`), interior start point (`teleport`), menu/enter coordinates, and vehicle spawn slots — all garages sharing an IPL id share those interior points. On first start, known legacy ids (`impexp_mid`, `impexp_large`, `eclipse_10car`, `lom_bank`) get their `ipl_names`/`teleport` seeded automatically if empty.

#### `server/config.lua` — Discord logging (server-only, never sent to clients)

| Option                 | Type    | Default                | Description                                                             |
| ---------------------- | ------- | ---------------------- | ----------------------------------------------------------------------- |
| `Config.Logs.enabled`  | boolean | `true`                 | Master switch for Discord webhook logging                               |
| `Config.Logs.console`  | boolean | `true`                 | Also print every logged event to the server console                     |
| `Config.Logs.botName`  | string  | `'ATY Garage'`         | Webhook embed username                                                  |
| `Config.Logs.webhook`  | string  | *(set)*                | Default webhook URL used when a category has none set                   |
| `Config.Logs.webhooks` | table   | *(all categories set)* | Per-category webhook override: `vehicle`, `economy`, `impound`, `admin` |
| `Config.Logs.colors`   | table   | blue/green/red/yellow  | Embed color per category                                                |

**Security note:** the shipped `server/config.lua` currently has a live Discord webhook URL filled in for the default and every category. Since this file only lives server-side it isn't leaked to clients, but you should still replace it with your own server's webhook(s) (or blank them out) before going live — the checked-in URL should be treated as compromised.

### Exports

This resource does **not** currently register any of its own `exports(...)` (client or server). All feature access goes through:

* **NUI ⇄ server callbacks** via `modules.RegisterCallback` / `modules.TriggerCallback` (see Events below for the full callback list — these are internal to the resource's own UI, not meant to be called from other resources).
* The **`aty_garage_v2:refreshHouseGarages`** server event (see Events) as the one documented integration point for `ps-housing`.

It consumes exports from its dependencies: `exports.aty_lib:getModules()/getUtils()/getShared()`, and optionally `exports['qb-target']` / `exports.ox_target` and `exports['okokBilling']:CreateCustomInvoice(...)`.

### Events

#### Public integration hook

| Event                               | Side   | Direction                      | Description                                                                                                                                                               |
| ----------------------------------- | ------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aty_garage_v2:refreshHouseGarages` | Server | Trigger **into** this resource | Fire this from `ps-housing` (or any script) after a house's garage data changes to force an immediate rescan + broadcast, instead of waiting for `Housing.rescanInterval` |

#### Internal client/server sync (not intended for external use)

| Event                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Side                                                  | Description                                                                                                  |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `aty_garage_v2:getGarages`, `getVehicles`, `takeVehicle`, `storeVehicle`, `transferVehicle`, `getImpoundGarages`, `issueFine`, `beginImpound`, `impoundVehicle`, `bringVehicle`, `getLostVehicles`, `locateVehicle`, `retrieveLostVehicle`, `buyInsurance`, `cancelInsurance`, `giveKeys`, `getKeyHolders`, `revokeKeys`, `favoriteVehicle`, `isAdmin`, `getAdminData`, `buyGarage`, `saveGarage`, `deleteGarage`, `getIplList`, `saveIplCoords`, `sellGarage`, `transferOwnedGarage`, `getManageGarage`, `ejectVehicle`, `getPurchaseInfo`, `saveManageGarage`, `getGarageMembers`, `addGarageMember`, `removeGarageMember` | Server (`modules.RegisterCallback`)                   | NUI-driven request/response callbacks used by the resource's own client code                                 |
| `aty_garage_v2:client:setGarages`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | Client (`RegisterNetEvent`)                           | Server pushes the player's currently visible garage list                                                     |
| `aty_garage_v2:client:openAdmin`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Client (`RegisterNetEvent`)                           | Fired by the server-registered `AdminCommand` once the framework permission check passes; opens the admin UI |
| `aty_garage_v2:client:openIplEditor`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Client (`RegisterNetEvent`)                           | Fired by the server-registered `IplCommand`; opens the IPL editor UI                                         |
| `aty_garage_v2:client:receiveKeys` / `aty_garage_v2:client:removeKeys`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Client (`RegisterNetEvent`)                           | Notifies a player they were given/revoked persistent keys to a plate                                         |
| `aty_garage_v2:vehicleDestroyed`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Server (`RegisterNetEvent`), triggered by client      | Client reports a park-in-place vehicle exploded, so the server marks it `destroyed` in `aty_garage_vehmeta`  |
| `aty_garage_v2:server:syncMileage`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | Server (`RegisterNetEvent`), triggered by client      | Rate-limited (max once/10s, capped at 30 miles/sync) mileage delta sync                                      |
| `aty_garage_v2:targetReady`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | Client (`AddEventHandler`/`TriggerEvent`, local only) | Internal signal that target-mode detection finished, used to register target zones/global-vehicle options    |
| `QBCore:Client:OnJobUpdate`, `esx:setJob`, `QBCore:Client:OnGangUpdate`, `QBCore:Client:OnPlayerLoaded`, `esx:playerLoaded`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | Client (listened to, not emitted)                     | Trigger a garage-list refresh on job/gang change or player load                                              |

### Commands

| Command                                      | Side                            | Default restriction                                                                                        | Description                                                                                               |
| -------------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `/garageadmin` (`Config.AdminCommand`)       | Client (registered server-side) | QBCore perms `Config.QBPermission` (`admin`, `god`) / ESX groups `Config.ESXGroup` (`admin`, `superadmin`) | Opens the garage creation/admin menu                                                                      |
| `/garageipl` (`Config.IplCommand`)           | Client (registered server-side) | Same as above                                                                                              | Opens the IPL interior editor directly                                                                    |
| `/garageblips` (`Config.Blip.toggleCommand`) | Client                          | None (any player)                                                                                          | Toggles the player's own garage blip visibility; preference stored in a resource KVP                      |
| `/impound`                                   | Client                          | None enforced at command level; internally requires `Config.Police.jobs`                                   | Opens the impound/fine flow for the vehicle you're in, or the nearest one                                 |
| `/garagedebug`                               | Client                          | **Admin only**                                                                                             | Prints interaction mode, target detection, garage count, and the nearest garage's distance/coords to F8   |
| `/garageprobe`                               | Client                          | **Admin only**                                                                                             | Places a bare test target zone to verify `ox_target`/`qb-target` renders options                          |
| `/iploc [id] [x y z]`                        | Client                          | **Admin only**                                                                                             | Loads an IPL and teleports you to its start point (or a candidate `x y z`) to verify interior coordinates |
| `/iplhere`                                   | Client                          | **Admin only**                                                                                             | Prints your current coords + interior id to F8 (for picking interior points)                              |

> The four diagnostic commands (`/garagedebug`, `/garageprobe`, `/iploc`, `/iplhere`) are **admin-gated**: each verifies the caller via the server `aty_garage_v2:isAdmin` callback (same `isAdmin()` used everywhere else — ACE `Config.AdminAce`, framework permissions, or `Config.AdminGroups`) before running. Non-admins get a "no permission" notification and the command does nothing.

### Features

* Multiple garage types: public, private (purchasable/manageable), job fleet, impound, and "lost vehicle" garages.
* Framework-agnostic (ESX / QBCore / Qbox) via `aty_lib`, including job-grade and QBCore gang-grade access locks per garage (ESX has no standard gang system, so job is used instead for gang-style access).
* Pre-built IPL interior garages (Import/Export mid & large, Eclipse Blvd 10-car, Underground/Lom Bank) with a dedicated in-game editor for menu/preview/spawn/zone points, a showroom of spawned display vehicles, and world-side vehicle spawn slots.
* Park-in-place persistence: vehicles stay physically parked where stored (frozen/invincible display entity), streamed in/out server-side based on player proximity, and survive restarts via the `aty_garage_parked` table.
* Vehicle insurance with configurable policy price/duration, and a "Lost Vehicles" recovery flow for destroyed vehicles (insured claim fee vs. uninsured recovery fee) as well as plain lost (not destroyed, not impounded) vehicles.
* Persistent key sharing (`aty_garage_keys`) so another player can take/store a plate without transferring ownership, plus revocation.
* Shared/manageable garage members (`aty_garage_members`) for co-owned private garages.
* Dynamic `ps-housing` house garages generated from the `properties` table (never written to `aty_garages`), periodically rescanned and instantly refreshable via `aty_garage_v2:refreshHouseGarages`.
* `ox_target` / `qb-target` support with automatic detection and marker-mode fallback.
* Discord webhook logging split into `vehicle`, `economy`, `impound`, and `admin` categories, queued to respect Discord rate limits.
* Vue NUI with category tabs, capacity indicator, and a cinematic vehicle preview camera with optional depth-of-field and a periodically-recaptured frosted blur background.
* Anti-dupe protections: per-plate operation locks (`plateLock`) and per-player/action rate limiting (`rateOk`) around take/store/transfer flows.
* Vehicle mileage tracking with a rate-limited client→server sync.
* Police fine and impound workflow for park-in-place vehicles, with configurable reasons/fees/impound duration and a billing bridge that tries `okokBilling` → `esx_billing` → QBCore `phone_invoices` → internal collection (or a custom hook).

### Troubleshooting

| Problem                                                                      | Solution                                                                                                                                                                                                                                                                                                                              |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Garages don't appear / menu won't open                                       | Run `/garagedebug` to check interaction mode, target detection, and the nearest garage's distance/coords. Confirm `aty_lib` started before `aty_garage_v2` in `server.cfg`.                                                                                                                                                           |
| Target option shows no interaction / no marker                               | If `Config.Interaction = 'target'` but neither `ox_target` nor `qb-target` is running, the resource falls back to marker mode automatically — check `/garagedebug` output for `ox=`/`qb=` resource state. Use `/garageprobe` to test whether the running target resource renders options at all.                                      |
| No IPL interiors in the garage/editor dropdown                               | Interiors are DB-backed. Create one in-game: `/garageipl` → dropdown → **+ Create New Interior** (enter an ID, label, and the IPL names to load, then pick the coordinates). Legacy ids are seeded on first start.                                                                                                                    |
| IPL teleport lands in an empty void                                          | The `teleport` (Interior Start Point) of that IPL record doesn't line up with the interior. Re-pick it in the IPL editor, or use `/iploc <id> <x> <y> <z>` to test candidate coordinates.                                                                                                                                             |
| House garages missing for a player                                           | Check `Config.Housing.enabled = true`, `Config.Housing.script = 'ps-housing'`, that `ps-housing` is running, and that the `properties` row has a non-empty `garage_data` and a matching `owner_citizenid`. Trigger `aty_garage_v2:refreshHouseGarages` server-side to force a rescan instead of waiting for `Housing.rescanInterval`. |
| Fines/impound don't bill externally                                          | Set `Config.Police.billing` to a specific mode to isolate the issue, or check that `okokBilling`/`esx_billing` is actually `started` (`GetResourceState`) or that the `phone_invoices` table exists for the `'qb'` mode. `'auto'` silently falls back to internal collection if all external attempts fail.                           |
| No Discord logs arriving                                                     | Check `server/config.lua` → `Config.Logs.enabled` and that `webhook`/`webhooks[category]` is a valid URL (replace the shipped example webhook with your own). Run the server-console-only `/garagelogtest` command to confirm delivery, and check `Config.Logs.console` output for the resolved settings.                             |
| Vehicle stuck as "destroyed" / can't retrieve                                | Open the Lost Vehicles menu: insured vehicles pay `Insurance.claimFee`, uninsured ones pay `Insurance.uninsuredFee` only if `Insurance.allowUninsured = true` — otherwise the vehicle cannot be recovered by design.                                                                                                                  |
| `/iploc`, `/iplhere`, `/garagedebug`, `/garageprobe` do nothing for a player | Working as intended — these diagnostics are **admin only** (verified via the server `aty_garage_v2:isAdmin` callback). Grant the player admin (ACE `Config.AdminAce`, framework `admin`/`god`/`superadmin`, or `Config.AdminGroups`) to use them.                                                                                     |
