> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aps-store.com/llms.txt
> Use this file to discover all available pages before exploring further.

# config.lua

Shared configuration: locale, feature flags, locations, NPC, blip, interaction, command, branding, screen, desktop apps, test settings, mini-games, business register, media stations, phone integration, penalty points and VIN.

> [!NOTE]
>
> Runs on: **shared**. This file stays editable when the resource is escrow-protected. Restart the resource after editing.

`config/config.lua` — the settings most servers change.

## Locale

```lua theme={null}
Config.Locale = 'en'   -- loads locales/en.lua and locales/ui/en.json
```

See [Localization](https://docs.aps-store.com/assets/aps_cityhall/guides/localization.md) to add a language.

## Feature flags

Set any module to `false` to remove it completely: its server callbacks are not registered and its app disappears from the PC.

```lua theme={null}
Config.Features = {
    licenseCenter    = true,  -- Theory tests + license management
    documentOrders   = true,  -- Document Office app
    vehicleInsurance = true,  -- Vehicle Insurance app
    bills            = true,  -- Bills & Fines app + giveBill export
    game             = true,  -- Mini-games (Paperwork Tower, Queue Rush, Typing Test) + leaderboards
    vehicleRegistry  = true,  -- Vehicle Registry app (list of the player's vehicles)
    businessRegister = true,  -- Business Register app (register / browse companies)
    government       = true,  -- Government Portal app for staff (config/government.lua)
    taxes            = true,  -- Finances app: personal + business taxes, city treasury (config/taxes.lua)
    penaltyPoints    = true,  -- penalty points on traffic tickets (Config.PenaltyPoints)
    vin              = true,  -- VINs for owned vehicles (Config.VIN)
    jobCenter        = true,  -- Job Center app: pick a job or apply (config/jobcenter.lua)
    paychecks        = false, -- salaries collected at City Hall; needs a framework edit (docs → Paychecks)
    npc              = true,  -- Spawn the NPC (+ blip) at every Config.Locations entry
                              -- (set false if you only open City Hall via the export / event / command)
}
```

## Locations

One clerk (and blip) is spawned per entry.

```lua theme={null}
Config.Locations = {
    { coords = vector4(-543.53, -207.07, 37.67, 206.34) },                 -- Rockford Hills
    { coords = vector4(1690.12, 3580.81, 35.62, 210.0), blip = false },   -- no map blip
    { coords = vector4(-140.5, 6290.2, 31.5, 135.0), model = 'a_f_y_business_02', label = 'Paleto Town Hall' },
}
```

| Key | Type | Description |
| - | - | - |
| `coords` | vector4 | Position and heading of the clerk. |
| `blip` | boolean | `false` hides the map blip for this location. |
| `label` | string | Optional blip name (default `Config.Blip.label`). |
| `model` / `scenario` | string | Optional per-location override of `Config.NPC`. |

## NPC & blip

```lua theme={null}
Config.NPC = {
    model      = 's_m_m_dockwork_01',
    scenario   = 'WORLD_HUMAN_CLIPBOARD',
    freeze     = true,
    invincible = true,
}

Config.Blip = { enabled = true, sprite = 545, color = 0, scale = 0.7, label = 'City Hall' }
```

## Interaction

```lua theme={null}
Config.Interaction = {
    type     = 'auto',        -- 'auto' | 'ox_target' | 'qb-target' | 'proximity' (3D text + key)
    distance = 2.0,
    icon     = 'fas fa-id-card',
    key      = 38,            -- E (proximity mode)
}
```

`'auto'` uses ox\_target, then qb-target, whichever is started. If the selected target isn't running, the proximity prompt is used automatically.

## Command

Optional chat command to open the PC from anywhere. While it's enabled, the City Hall location check ([Security](https://docs.aps-store.com/assets/aps_cityhall/configuration-files/config.server.lua.md)) is off.

```lua theme={null}
Config.Command = {
    enabled = false,
    name    = 'cityhall',
    ace     = nil,   -- e.g. 'aps_cityhall.open' → add_ace group.admin aps_cityhall.open allow
}
```

## Test settings

```lua theme={null}
Config.Settings = {
    questionsPerTest = 20,     -- questions drawn from each license's pool
    cooldownSeconds  = 3600,   -- wait after a failed test
    notifyType       = 'auto', -- 'auto' | 'esx' | 'qb' | 'ox_lib'
                               -- auto: ESX → esx, QBCore → qb, Qbox → ox_lib
}
```

***

## Branding & desktop

Make the PC your own: names, screen size, available apps and the default look.

### Branding

```lua theme={null}
Config.Branding = {
    cityName     = 'Los Santos',      -- "City of …" on the built-in websites
    orgName      = 'City Hall',
    osName       = 'City Hall OS',    -- Command Prompt, Control Panel
    monitorLabel = 'CITY HALL',       -- text on the monitor bezel ('' hides it)
    computerName = 'CITYHALL-PC07',
}
```

### Screen

```lua theme={null}
Config.UI = {
    width         = 80,     -- % of the game window
    height        = 80,
    welcomeScreen = true,   -- "Welcome" / "Logging off…" screens
    monitor       = true,   -- bezel + stand around the screen
}
```

> [!NOTE]
>
> Set `width = 100, height = 100, monitor = false` for a full-screen PC without the monitor frame.

### Desktop apps

Remove any built-in app from the PC. City Hall services are switched with [feature flags](https://docs.aps-store.com/assets/aps_cityhall/configuration-files/config.lua.md) instead.

```lua theme={null}
Config.DesktopApps = {
    browser = true, explorer = true, writer = true, sheet = true, paint = true,
    photos = true, media = true, cmd = true, calculator = true, notepad = true,
    settings = true,   -- Control Panel
}
```

### Desktop defaults

The look a player gets until they change it themselves in the Control Panel. Their own choices are saved on their PC and win over these defaults. "Restore default settings" in the Control Panel returns to these values.

| Key | Values | Default |
| - | - | - |
| `wallpaper` | `harmony` · `dusk` · `teal` · `graphite` | `harmony` |
| `glassColor` | `sky` · `twilight` · `sea` · `leaf` · `sun` · `pumpkin` · `ruby` · `violet` · `slate` · `frost` | `sky` |
| `transparency` | boolean | `true` |
| `intensity` | 0.0 – 1.0 | `0.6` |
| `crt` | boolean — old-monitor scanlines | `true` |
| `iconSize` | `large` · `medium` · `small` | `large` |
| `showGadgets` | boolean | `true` |
| `screensaver` | `none` · `bubbles` · `blank` | `none` |
| `screensaverMinutes` | 1 – 10 | `3` |
| `clock24` | boolean | `false` |
| `singleClick` | boolean | `false` |
| `avatar` | `flower` · `violet` · `person` | `flower` |

***

## Full View

Preview File Updated: 3.7.0 - 02.11.2026

```lua config/config.lua lines expandable theme={null}
Config = Config or {}

-- ─────────────────────────────────────────────
--  Locale
--  Language code: loads locales/<code>.lua (server and client messages)
--  and locales/ui/<code>.json (everything inside the PC).
--  Example: 'de' loads locales/de.lua and locales/ui/de.json.
--  Included: 'en' (English), 'de' (Deutsch).
--  Missing files or keys fall back to English. See docs → Localization.
-- ─────────────────────────────────────────────
Config.Locale = 'en'

-- ─────────────────────────────────────────────
--  Feature flags
--  Set any value to false to completely disable that module.
--  Disabled modules will not register their server callbacks,
--  will not appear as apps in the NUI, and (where applicable)
--  will not spawn the NPC or blip.
-- ─────────────────────────────────────────────
Config.Features = {
    licenseCenter    = true,  -- Theory tests + license management
    documentOrders   = true,  -- Document Office app
    vehicleInsurance = true,  -- Vehicle Insurance app
    bills            = true,  -- Bills & Fines app + giveBill export
    game             = true,  -- Mini-games (Paperwork Tower, Queue Rush, Typing Test) + leaderboards
    vehicleRegistry  = true,  -- Vehicle Registry app (list of the player's vehicles)
    businessRegister = true,  -- Business Register app (register / browse companies)
    government       = true,  -- Government Portal app for staff (config/government.lua)
    taxes            = true,  -- Finances app: personal + business taxes, city treasury (config/taxes.lua)
    penaltyPoints    = true,  -- penalty points on traffic tickets (Config.PenaltyPoints)
    vin              = true,  -- VINs for owned vehicles (Config.VIN)
    jobCenter        = true,  -- Job Center app: pick a job or apply (config/jobcenter.lua)
    paychecks        = false, -- salaries collected at City Hall; needs a framework edit (docs → Paychecks)
    npc              = true,  -- Spawn the NPC (+ blip) at every Config.Locations entry
                              -- (set false if you only open City Hall via the export / event / command)
}

-- ─────────────────────────────────────────────
--  Locations
--  One NPC (and blip) is spawned per entry. Add as many as you like.
--    coords  vector4(x, y, z, heading)
--    blip    false to hide the map blip for this location (default true)
--    model / scenario  optional per-location override of Config.NPC
-- ─────────────────────────────────────────────
Config.Locations = {
    { coords = vector4(-543.5352, -207.0693, 37.6651, 206.3393) },   -- Rockford Hills
    -- { coords = vector4(1690.12, 3580.81, 35.62, 210.0), blip = true },  -- Sandy Shores (example)
}

-- ─────────────────────────────────────────────
--  NPC defaults (used for every location)
-- ─────────────────────────────────────────────
Config.NPC = {
    model      = 's_m_m_dockwork_01',
    scenario   = 'WORLD_HUMAN_CLIPBOARD',
    freeze     = true,
    invincible = true,
}

-- ─────────────────────────────────────────────
--  Map blip
-- ─────────────────────────────────────────────
Config.Blip = {
    enabled = true,
    sprite  = 545,
    color   = 0,
    scale   = 0.7,
    label   = 'City Hall',
}

-- ─────────────────────────────────────────────
--  Chat command to open the City Hall PC from anywhere
--  (e.g. for admins, or a laptop item via your own script).
--    ace  nil = everyone may use it; or an ace like 'aps_cityhall.open'
--         (add_ace group.admin aps_cityhall.open allow)
--  Other ways to open it:
--    client: exports['aps_cityhall']:OpenCityHall()
--    client: TriggerEvent('aps_cityhall:open')
--    server: TriggerClientEvent('aps_cityhall:open', source)
-- ─────────────────────────────────────────────
Config.Command = {
    enabled = false,
    name    = 'cityhall',
    ace     = nil,
}

-- ─────────────────────────────────────────────
--  Interaction
--    type     'auto'       — ox_target → qb-target → proximity (first one started)
--             'ox_target' | 'qb-target'  — that target (proximity if not started)
--             'proximity'  — 3D text + E key press
--    distance  interaction radius in units
--    icon      target icon class (FontAwesome)
--    key       FiveM control index used in proximity mode (38 = E)
-- ─────────────────────────────────────────────
Config.Interaction = {
    type     = 'auto',
    distance = 2.0,
    icon     = 'fas fa-id-card',
    key      = 38,
}

-- ─────────────────────────────────────────────
--  Branding — names shown inside the PC
--    cityName      used on the built-in websites ("City of …")
--    orgName       the organisation running the PC
--    osName        operating system name (Command Prompt, Control Panel)
--    monitorLabel  text on the monitor's bezel ('' to hide)
--    computerName  shown in Control Panel → System
-- ─────────────────────────────────────────────
Config.Branding = {
    cityName     = 'Los Santos',
    orgName      = 'City Hall',
    osName       = 'City Hall OS',
    monitorLabel = 'CITY HALL',
    computerName = 'CITYHALL-PC07',
}

-- ─────────────────────────────────────────────
--  Screen
--    width / height  size of the PC screen in % of the game window
--    welcomeScreen   show the "Welcome" / "Logging off…" screens
--    monitor         draw the monitor bezel + stand around the screen
-- ─────────────────────────────────────────────
Config.UI = {
    width         = 80,
    height        = 80,
    welcomeScreen = true,
    monitor       = true,
}

-- ─────────────────────────────────────────────
--  Desktop apps — set false to remove an app from the PC.
--  (City Hall services are switched with Config.Features above.)
-- ─────────────────────────────────────────────
Config.DesktopApps = {
    browser    = true,   -- Internet (built-in city websites)
    explorer   = true,   -- File Explorer
    writer     = true,   -- Writer (word processor)
    sheet      = true,   -- Spreadsheet
    paint      = true,   -- Paint
    photos     = true,   -- Photo Viewer
    media      = true,   -- Media Player (see Config.MediaStreams)
    cmd        = true,   -- Command Prompt
    calculator = true,   -- Calculator
    notepad    = true,   -- Notepad
    settings   = true,   -- Control Panel
}

-- ─────────────────────────────────────────────
--  Desktop defaults — the look a player gets before changing anything
--  in the Control Panel (their own changes are saved on their PC).
--    wallpaper    'harmony' | 'dusk' | 'teal' | 'graphite'
--    glassColor   'sky' | 'twilight' | 'sea' | 'leaf' | 'sun' | 'pumpkin'
--                 | 'ruby' | 'violet' | 'slate' | 'frost'
--    transparency true / false     intensity  0.0 – 1.0
--    crt          old-monitor scanline effect
--    iconSize     'large' | 'medium' | 'small'
--    showGadgets  clock + calendar on the desktop
--    screensaver  'none' | 'bubbles' | 'blank'   screensaverMinutes  1–10
--    clock24      24-hour clock      singleClick  open icons with one click
--    avatar       'flower' | 'violet' | 'person'
-- ─────────────────────────────────────────────
Config.DesktopDefaults = {
    wallpaper          = 'harmony',
    glassColor         = 'sky',
    transparency       = true,
    intensity          = 0.6,
    crt                = true,
    iconSize           = 'large',
    showGadgets        = true,
    screensaver        = 'none',
    screensaverMinutes = 3,
    clock24            = false,
    singleClick        = false,
    avatar             = 'flower',
}

-- ─────────────────────────────────────────────
--  Global test settings
-- ─────────────────────────────────────────────
Config.Settings = {
    questionsPerTest = 20,      -- questions randomly picked per test session
    cooldownSeconds  = 3600,    -- retry cooldown after a failed test (seconds)
    notifyType       = 'auto',  -- 'auto' (vms_notifyv2 / okokNotify when started, else framework default)
                                --  | 'esx' | 'qb' | 'ox_lib' | 'vms_notifyv2' | 'okokNotify'
}

-- ─────────────────────────────────────────────
--  Mini-games
--    leaderboardSize   entries shown in each game's Top list
--    games             anti-cheat rules per game. A submitted score is
--                      rejected when it's impossible for the round length:
--      minSecondsPerPoint  round must last at least score × this
--      minDuration         round must last at least this many seconds
--      maxScore            highest score accepted
--      roundSeconds        (typing) length of a round; the server then
--                          requires roundSeconds - 5 unless minDuration is set
-- ─────────────────────────────────────────────
Config.Game = {
    leaderboardSize = 10,
    games = {
        tower  = { minSecondsPerPoint = 0.25 },              -- Paperwork Tower (height)
        snake  = { minSecondsPerPoint = 0.20 },              -- Queue Rush (tickets collected)
        typing = { roundSeconds = 60, maxScore = 250 },      -- Typing Test (words per minute)
    },
}

-- ─────────────────────────────────────────────
--  Business Register
--    fee           registration fee taken from Config.MoneyAccount (0 = free)
--    maxPerPlayer  active businesses one player may own
--    categories    choices shown in the registration form
-- ─────────────────────────────────────────────
Config.Business = {
    fee          = 5000,
    maxPerPlayer = 3,
    categories   = {
        'Retail', 'Food & Drink', 'Automotive', 'Services',
        'Entertainment', 'Real Estate', 'Transport', 'Other',
    },

    -- ── Permits ─────────────────────────────────
    -- Licenses a business can hold. Other scripts check them with
    -- exports['aps_cityhall']:hasBusinessPermit / playerHasPermit.
    --   label / description   shown in the Business Register
    --   categories            business categories that may apply (nil = all)
    --   review                true = the Government Portal must approve the
    --                         application (and renewals); false = granted on payment
    --   requiresLicenses      the owner must hold these City Hall licenses
    --                         (ids from config/licenses.lua)
    --   options               validity choices { label, days, price }
    --                         renewing while valid adds the days on top
    permits = {
        alcohol = {
            label       = 'Alcohol License',
            description = 'Sell and serve alcoholic drinks.',
            categories  = { 'Food & Drink', 'Retail', 'Entertainment' },
            review      = true,
            options     = {
                { label = '30 Days', days = 30,  price = 5000 },
                { label = '1 Year',  days = 365, price = 45000 },
            },
        },
        tobacco = {
            label       = 'Tobacco License',
            description = 'Sell cigarettes, cigars and other tobacco products.',
            categories  = { 'Retail', 'Food & Drink' },
            review      = false,
            options     = {
                { label = '30 Days', days = 30,  price = 2500 },
                { label = '1 Year',  days = 365, price = 22000 },
            },
        },
        transport = {
            label            = 'Transport License',
            description      = 'Carry passengers or goods for money (taxi, bus, delivery, trucking).',
            categories       = { 'Transport' },
            review           = true,
            requiresLicenses = { 'car' },
            options          = {
                { label = '90 Days', days = 90,  price = 7500 },
                { label = '1 Year',  days = 365, price = 25000 },
            },
        },
        food = {
            label       = 'Food Hygiene Certificate',
            description = 'Prepare and sell food to the public.',
            categories  = { 'Food & Drink' },
            review      = false,
            options     = {
                { label = '1 Year', days = 365, price = 3000 },
            },
        },
    },

    -- ── Business vehicles ───────────────────────
    --   The owner registers their own vehicles to an active business.
    --   setJobColumn  also write the business's linked job (set by the
    --                 government in the portal) into the owned vehicles
    --                 table, so job garages see the vehicle. Cleared again
    --                 when the vehicle is removed.
    --   jobColumn     that column ('job' exists on ESX owned_vehicles;
    --                 QBCore has none by default — then nothing is written)
    vehicles = {
        enabled        = true,
        fee            = 500,     -- per vehicle registered
        maxPerBusiness = 10,
        setJobColumn   = true,
        jobColumn      = 'job',
    },

    -- ── Employees ───────────────────────────────
    --   The owner adds a nearby player by their server ID (they can be removed
    --   again any time). Employees see the business under
    --   "My Businesses" and count for playerHasPermit / isBusinessEmployee.
    employees = {
        enabled        = true,
        maxPerBusiness = 15,
        maxDistance    = 10.0,   -- the new employee must stand this close to the owner (0 = anywhere)
    },

    -- ── Registration certificate ────────────────
    --   An inventory item with the business name, owner and valid permits
    --   (ox_inventory: as metadata). The item must exist in your inventory.
    certificate = {
        enabled = true,
        item    = 'business_certificate',
        price   = 100,
    },
}

-- ─────────────────────────────────────────────
--  Media Player stations
--  Audio streams / files players can pick in the Media Player app.
--  Use direct links to audio (mp3 / aac / ogg streams). Only the player
--  using the PC hears it.
-- ─────────────────────────────────────────────
Config.MediaStreams = {
    -- { label = 'My Radio', url = 'https://example.com/stream.mp3' },
}

-- ─────────────────────────────────────────────
--  Phone integration
--                        notifications  mail  phone app  PC Mail app
--    sd-phone                 yes        yes     yes      read + send
--    lb-phone                 yes        yes     yes      full
--    17mov_Phone              yes        yes     yes      read-only
--    yseries                  yes        yes     yes      -
--    qs-smartphone(-pro)      yes        yes     yes      -
--    gksphone                 yes        yes     -        -
--    qb-phone                 yes        yes     -        -
--    custom                   Config.PhoneCustom in config/config.server.lua
--                             (connect any other phone, e.g. okokPhone)
--
--    system         'auto' (first one that is started; sd-phone is checked
--                   first because it also answers to other phones' names)
--                   | a resource name from the list above | 'custom' | 'none'
--    app            a read-only "City Hall" app inside the phone: shows unpaid
--                   bills and expiring insurance & documents, and can set a
--                   waypoint to the nearest City Hall. Players can't pay or
--                   order anything from the phone — they have to visit City Hall.
--    notify         phone notifications (true / false per event)
--    mail           also send an email to the phone's mail app
--    texts          notification / mail texts (%s placeholders are filled in order)
-- ─────────────────────────────────────────────
Config.Phone = {
    system = 'auto',

    -- Mail app on the PC that shows the phone's mailbox.
    --   lb-phone     full two-way sync: read, write, reply, delete (uses lb-phone's mail tables)
    --   sd-phone     read, write, reply (mark-as-read and delete happen on the phone)
    --   17mov_Phone  read-only: inbox / sent / favourites / bin (replies are written on the phone)
    -- Hidden when no supported phone is running.
    mailApp = {
        enabled       = true,
        maxLength     = 4000,   -- max characters per mail written on the PC
        sendCooldown  = 10,     -- seconds between two mails from the same player
        listLimit     = 100,    -- mails loaded per folder
    },

    app = {
        enabled        = true,
        identifier     = 'aps_cityhall',
        name           = 'City Hall',
        description    = 'Bills, documents, insurance and licenses at a glance.',
        developer      = 'City Hall',
    },

    notify = {
        billIssued         = true,
        billPaid           = true,
        licenseEarned      = true,
        documentOrdered    = true,
        insurancePurchased = true,
        businessRegistered = true,
        businessPending    = true,
        businessApproved   = true,
        businessRejected   = true,
        businessSuspended  = true,
        licenseRevoked     = true,
        permitPending      = true,
        permitApproved     = true,
        permitRejected     = true,
        permitRevoked      = true,
        employeeAdded      = true,
        businessInspected  = true,
        taxIssued          = true,
        taxCollected       = true,
        licenseSuspended   = true,
        licenseReinstated  = true,
        pointsAdded        = true,
        paycheckReceived   = false,
        applicationReceived = true,
        applicationAccepted = true,
        applicationRejected = true,
    },

    mail = {
        billIssued = true,
        sender     = 'City Hall',
    },

    texts = {
        billIssued         = { title = 'New bill',            content = 'You received a bill of $%s from %s.' },
        billPaid           = { title = 'Bill paid',           content = 'Your payment of $%s was received.' },
        licenseEarned      = { title = 'License issued',      content = 'Your %s has been issued.' },
        documentOrdered    = { title = 'Document ready',      content = 'Your %s is valid for %s days.' },
        insurancePurchased = { title = 'Vehicle insured',     content = '%s is insured for %s days.' },
        businessRegistered = { title = 'Business registered', content = '%s is now listed in the Business Register.' },
        businessPending    = { title = 'Registration received', content = '%s is waiting for review by the government.' },
        businessApproved   = { title = 'Business approved',   content = '%s has been approved and is now listed.' },
        businessRejected   = { title = 'Business rejected',   content = '%s was not approved: %s' },
        businessSuspended  = { title = 'Business suspended',  content = '%s has been suspended: %s' },
        licenseRevoked     = { title = 'License revoked',     content = 'Your %s has been revoked.' },
        permitPending      = { title = 'Permit application',  content = 'Your %s application for %s is waiting for review.' },
        permitApproved     = { title = 'Permit granted',      content = '%s granted for %s.' },
        permitRejected     = { title = 'Permit rejected',     content = '%s for %s was not approved: %s' },
        permitRevoked      = { title = 'Permit revoked',      content = '%s for %s has been revoked: %s' },
        employeeAdded      = { title = 'New job',             content = 'You now work for %s.' },
        businessInspected  = { title = 'Business inspection', content = '%s was inspected: %s.' },
        taxIssued          = { title = 'New tax',             content = '%s: $%s to pay at City Hall.' },
        taxCollected       = { title = 'Tax collected',       content = 'Overdue %s: $%s was taken from your bank account.' },
        licenseSuspended   = { title = 'License suspended',   content = 'Your %s is suspended until %s.' },
        licenseReinstated  = { title = 'License reinstated',  content = 'Your %s is valid again.' },
        pointsAdded        = { title = 'Penalty points',      content = '%s penalty points added. You now have %s of %s.' },
        paycheckReceived   = { title = 'Salary',              content = '$%s salary is waiting for you at City Hall.' },
        applicationReceived = { title = 'New application',    content = '%s applied for %s.' },
        applicationAccepted = { title = 'Application accepted', content = 'You were hired at %s.' },
        applicationRejected = { title = 'Application rejected', content = 'Your application to %s was not successful.' },
        billMailSubject    = 'Bill from %s',
        billMailBody       = 'You received a %s of $%s from %s.\n\n%s\n\nPay it on any City Hall computer.',
        billMailDue        = 'Please pay within %s days.',
        billMailNoDue      = 'There is no due date.',
    },
}

-- ─────────────────────────────────────────────
--  Penalty points (Config.Features.penaltyPoints)
--  Traffic tickets with `penaltyPointsCount` (giveBill) and the
--  addPenaltyPoints export add points.
--    limit         points at which licenses are taken
--    expireDays    points expire after this many days (0 = never)
--    atLimit       'suspend' | 'revoke'
--    suspendDays   suspension length when atLimit = 'suspend'
--    licenses      City Hall license ids affected at the limit
--    resetAtLimit  clear the points once the limit was applied
-- ─────────────────────────────────────────────
Config.PenaltyPoints = {
    limit        = 24,
    expireDays   = 30,
    atLimit      = 'suspend',
    suspendDays  = 30,
    licenses     = { 'car', 'motorcycle', 'truck' },
    resetAtLimit = true,
}

-- ─────────────────────────────────────────────
--  VIN (Config.Features.vin)
--  A 17-character vehicle identification number for every owned vehicle,
--  shown in the Vehicle Registry and to staff.
--    column / hiddenColumn  columns in the owned vehicles table
--    autoMigrate            add the columns automatically if missing
--    checkCommand           look up the VIN of the closest vehicle
--                           (jobs: job → lowest grade; nil = everyone)
--  Console: `_generatevin` gives every vehicle without a VIN one.
-- ─────────────────────────────────────────────
Config.VIN = {
    column       = 'vin',
    hiddenColumn = 'vin_hidden',
    autoMigrate  = true,
    checkCommand = {
        enabled  = true,
        name     = 'checkvin',
        distance = 5.0,
        jobs     = { police = 0, sheriff = 0 },
    },
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.