# esx_society (/docs/addons/esx_society)

`esx_society` turns an ESX job into a company. It links a job to a shared bank account, adds a boss menu for hiring, firing, promotions, salaries, grade labels and uniforms, and lets bosses launder black money into the company account. Job resources such as [esx_policejob](/docs/addons/esx_policejob), [esx_taxijob](/docs/addons/esx_taxijob) and [esx_mechanicjob](/docs/addons/esx_mechanicjob) register their society with it and open its boss menu.

## Overview [#overview]

| Fact | Value |
| --- | --- |
| Resource | `esx_society` |
| Side | Server (API, validation, money) and client (boss menu) |
| Dependencies | `esx_lib`, `es_extended`, `cron`, `esx_addonaccount` |
| Uses at runtime | `oxmysql`, `esx_datastore` (society garage), `skinchanger` (uniform saving) |
| SQL | `esx_society.sql` (`society_moneywash` table) |

A society is a plain record kept in memory:

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Job name, for example `taxi`. Must match `jobs.name`. |
| `label` | string | Display name. |
| `account` | string | Shared `esx_addonaccount` account, usually `society_<job>`. |
| `datastore` | string | Shared `esx_datastore` store, usually `society_<job>`. Used for the society garage. |
| `inventory` | string | Shared `esx_addoninventory` inventory, usually `society_<job>`. |
| `data` | table | Free-form options. The official jobs pass `{ type = 'public' }` or `{ type = 'private' }`. esx_society stores this table but does not read it. |

Societies are not saved in the database. Every job resource must register its society again on every start.

## Installation [#installation]

<Steps>
<Step>
### Install the dependencies [#install-the-dependencies]

Make sure `oxmysql`, `es_extended`, `esx_lib`, `cron` and [esx_addonaccount](/docs/addons/esx_addonaccount) are installed. Install [esx_datastore](/docs/addons/esx_datastore) and [esx_addoninventory](/docs/addons/esx_addoninventory) too, because the official job resources create society datastores and inventories.
</Step>
<Step>
### Import the SQL [#import-the-sql]

Import `esx_society.sql`. It creates the `society_moneywash` table. On start, esx_society also creates `society_moneywash_operations` and an index `esx_society_employees` on `users (job, job_grade, identifier)` by itself.

```sql title="esx_society.sql"
CREATE TABLE `society_moneywash` (
	`id` int NOT NULL AUTO_INCREMENT,
	`identifier` varchar(60) NOT NULL,
	`society` varchar(60) NOT NULL,
	`amount` int NOT NULL,
	PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
</Step>
<Step>
### Start it before every job resource [#start-it-before-every-job-resource]

```bash title="server.cfg"
ensure cron
ensure esx_addonaccount
ensure esx_datastore
ensure esx_addoninventory
ensure esx_society
# job resources after this line
ensure esx_policejob
ensure esx_taxijob
```
</Step>
</Steps>

<Callout type="warn" title="InnoDB required for money washing">
On start the money wash module checks that `users`, `society_moneywash` and `addon_account_data` use the InnoDB engine. If one does not, it prints `[esx_society] Moneywash disabled: ...` and every wash request fails with "invalid amount". The rest of the resource keeps working.
</Callout>

## Configuration [#configuration]

All keys live in `config.lua`.

| Key | Default | Description |
| --- | --- | --- |
| `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language of the menus and notifications. |
| `Config.EnableESXIdentity` | `true` | Show `firstname lastname` from the `users` table in the employee list. Set to `false` if you do not use [esx_identity](/docs/core/esx_identity). |
| `Config.MaxSalary` | `3500` | Highest salary a boss can set for a grade. Must be a whole number from `0` up to this value. |
| `Config.MaxJobGradeLabelLength` | `40` | Grade labels set by bosses are trimmed to this length. |
| `Config.JobGradeUpdateCooldown` | `1500` | Minimum delay in ms between two salary or label changes by the same boss for the same job. `0` disables the limit. |
| `Config.SocietyGarageDistance` | `12.0` | Max distance in meters from a garage zone for the society garage events. |
| `Config.SocietyGarageZones` | `mechanic` and `taxi` zones | Per-society list of `vector3` points where the society garage may be used. |
| `Config.BossGrades` | `{ ['boss'] = true }` | Grade **names** (`job_grades.name`) that get boss access. Add more names to give access to several grades. |
| `Config.EnableUniformManagement` | `false` | Show uniform management in every boss menu. You can also enable it per job with the `uniforms` option. |
| `Config.UniformSaveCooldown` | `30000` | Minimum delay in ms between two uniform saves for the same job. |
| `Config.UniformDrawableMax` | `2000` | Highest accepted drawable ID for clothing components. Keep it high for custom clothing packs. |
| `Config.UniformTextureMax` | `255` | Highest accepted texture ID (components ending in `_2`). |
| `Config.UniformPropMax` | `255` | Highest accepted prop ID for `helmet_1`, `glasses_1`, `watches_1`, `bracelets_1` and `ears_1`. Props may also be `-1` (none). |
| `Config.UniformComponentLimits` | `{}` | Per-component overrides, for example `helmet_1 = { min = -1, max = 250 }`. |
| `Config.UniformComponents` | 30 skin keys | The skinchanger components that make up a uniform: `tshirt_1` … `ears_2`. A saved uniform must contain every one of them. |

```lua title="config.lua"
Config.BossGrades = {
    ['boss'] = true,
    ['chef'] = true,   -- give a second grade access to the boss menu
}
```

## How it works [#how-it-works]

- **Boss check**: a player has boss access to a society when `xPlayer.getJob().name == society.name` and the grade name is in `Config.BossGrades`. Every server event and callback repeats this check. The client menu is only a view.
- **Account**: balance, deposits and withdrawals go through the shared [esx_addonaccount](/docs/addons/esx_addonaccount) account named in `society.account`.
- **Salaries and labels** are written to `job_grades` and then reloaded with `ESX.RefreshJob(job)` (or `ESX.RefreshJobs()` as fallback), so changes apply without a restart.
- **Employees** are listed in pages of 50: online players first, then offline players from the `users` table.
- **Money washing** is queued in `society_moneywash_operations` and paid by a [cron](/docs/core/cron) task every day at 03:00 (`TriggerEvent('cron:runAt', 3, 0, WashMoneyCRON)`).
- **Society payouts**: when es_extended's `Config.EnableSocietyPayouts` is `true`, the paycheck module calls `esx_society:getSociety` and pays salaries from the society account. See [Money and accounts](/docs/guides/money-and-accounts).

## Server API [#server-api]

### registerSociety (export and event) [#registersociety-export-and-event]

Registers a job as a society. Call it once when your job resource starts.

```lua
exports.esx_society:registerSociety(name, label, account, datastore, inventory, data)
TriggerEvent('esx_society:registerSociety', name, label, account, datastore, inventory, data)
```

| Name | Type | Description |
| --- | --- | --- |
| `name` | string | Job name. |
| `label` | string | Display label. |
| `account` | string | Shared addon account name. |
| `datastore` | string | Shared datastore name. |
| `inventory` | string | Shared addon inventory name. |
| `data` | table | Free options, for example `{ type = 'private' }`. |

Returns nothing. Registering the same name twice prints a warning and keeps the first entry.

```lua title="server/main.lua"
exports.esx_society:registerSociety('taxi', 'Taxi', 'society_taxi', 'society_taxi', 'society_taxi', { type = 'public' })
```

The event is a local server event (`AddEventHandler`). Clients cannot trigger it.

### GetSociety (export) [#getsociety-export]

```lua
local society = exports.esx_society:GetSociety(name)
```

| Name | Type | Description |
| --- | --- | --- |
| `name` | string | Society (job) name. |

Returns the society table or `nil`.

```lua title="server/main.lua"
local society = exports.esx_society:GetSociety('police')

if society then
    TriggerEvent('esx_addonaccount:getSharedAccount', society.account, function(account)
        print(('%s has $%s'):format(society.label, account.money))
    end)
end
```

### esx_society:getSociety (event) [#esx_societygetsociety-event]

Callback style lookup, used by es_extended's paycheck module.

```lua
TriggerEvent('esx_society:getSociety', name, cb)
```

| Name | Type | Description |
| --- | --- | --- |
| `name` | string | Society name. |
| `cb` | function | Receives the society table or `nil`. |

### esx_society:getSocieties (event) [#esx_societygetsocieties-event]

```lua
TriggerEvent('esx_society:getSocieties', function(societies)
    for _, society in ipairs(societies) do
        print(society.name, society.account)
    end
end)
```

Returns the array of all registered societies.

## Client API [#client-api]

### esx_society:openBossMenu [#esx_societyopenbossmenu]

Opens the boss menu (ESX context menu). Use it from a marker or target zone in your job resource.

```lua
TriggerEvent('esx_society:openBossMenu', society, close, options)
```

| Name | Type | Description |
| --- | --- | --- |
| `society` | string | Society name. Must equal the player's job name. |
| `close` | function or nil | Passed to `ESX.OpenContext` as the close handler of the main menu. Called when the player closes it. |
| `options` | table or nil | Menu entries to show. Missing keys use the defaults below. |

| Option | Default | Entry |
| --- | --- | --- |
| `checkBal` | `true` | Check society balance |
| `withdraw` | `true` | Withdraw money |
| `deposit` | `true` | Deposit money |
| `wash` | `true` | Wash black money |
| `employees` | `true` | Employee list and recruiting |
| `salary` | `true` | Salary per grade |
| `grades` | `true` | Rename grade labels |
| `uniforms` | `Config.EnableUniformManagement` | Save the worn outfit as a grade uniform |

```lua title="client/main.lua"
TriggerEvent('esx_society:openBossMenu', 'taxi', function()
    ESX.CloseContext()
end, { wash = false, uniforms = true })
```

The menu first asks the server through the `esx_society:isBoss` callback. Non-bosses see nothing.

## Network events (client to server) [#network-events-client-to-server]

These are `RegisterServerEvent` handlers, so any client can send them. All of them validate on the server.

<Callout type="info" title="Security">
Each event below is reachable from a modified client. The checks listed are what protects you. If you fork esx_society, keep them.
</Callout>

| Event | Parameters | Server checks |
| --- | --- | --- |
| `esx_society:checkSocietyBalance` | `society` | Boss access to that society. Shows the balance as a notification. |
| `esx_society:withdrawMoney` | `society`, `amount` | Society exists, amount is a positive number (rounded), boss access, account has enough money. |
| `esx_society:depositMoney` | `society`, `amount` | Society exists, positive amount, player's **job** equals the society (any grade), player has the cash. |
| `esx_society:washMoney` | `society`, `amount` | Positive amount, boss access, enough `black_money`, wash module ready. |
| `esx_society:putVehicleInGarage` | `society`, `vehicle` | Same job, within `Config.SocietyGarageDistance` of a `Config.SocietyGarageZones` point, valid plate, the player is in or next to (10 m) a vehicle with that plate and model. Deletes the entity and stores it in the society datastore key `garage`. |
| `esx_society:removeVehicleFromGarage` | `society`, `vehicle` | Same job and garage zone checks, valid plate. Removes the entry from `garage`. |

```lua title="client/main.lua"
TriggerServerEvent('esx_society:depositMoney', 'taxi', 500)
```

<Callout type="warn">
`depositMoney` only requires the job, not a boss grade. Every employee can pay into the company account, even though the boss menu shows the button only to bosses.
</Callout>

## Server callbacks [#server-callbacks]

Registered with `xLib.callback.registerCompat`. Call them with `ESX.TriggerServerCallback`, `ESX.AwaitServerCallback` or `xLib.callback`. See [Callbacks](/docs/guides/callbacks).

| Callback | Arguments | Returns | Access |
| --- | --- | --- | --- |
| `esx_society:isBoss` | `job` | `boolean` | Caller's own job and boss grade. |
| `esx_society:getSocietyMoney` | `society` | `number` (0 when denied) | Boss of that society. |
| `esx_society:getEmployees` | `society`, `cursor?` | `employees`, `nextCursor` | Boss of that society. One request per player at a time. |
| `esx_society:getOnlinePlayers` | `society` | array of `{ source, identifier, name, job = { name, grade, grade_name } }` | Boss of that society. Cached for 1 second. |
| `esx_society:getJob` | `job` | job table with `grades` sorted by grade, or `false` | No check. Returns public job data. |
| `esx_society:setJob` | `identifier`, `job`, `grade`, `actionType` | `boolean` | Boss. See below. |
| `esx_society:setJobSalary` | `job`, `grade`, `salary` | `boolean` | Boss of that job, valid grade, salary 0 to `Config.MaxSalary`, cooldown. |
| `esx_society:setJobLabel` | `job`, `grade`, `label` | `boolean` | Boss of that job, valid grade, label trimmed to `Config.MaxJobGradeLabelLength`, cooldown. |
| `esx_society:setJobUniform` | `job`, `grade`, `skin` | `boolean` | Boss of that job, complete and in-range skin, sex matches identity, `Config.UniformSaveCooldown`. `grade = -1` saves for all grades. |
| `esx_society:getVehiclesInGarage` | `society` | array of stored vehicles | Same job, inside a garage zone. |

### esx_society:setJob rules [#esx_societysetjob-rules]

`actionType` must be `'hire'`, `'promote'` or `'fire'`.

- **hire / promote**: `job` must be the boss's own job and `grade` must exist and be lower than or equal to the boss's grade. Hiring needs the target online.
- **fire**: `job` must be `'unemployed'` and `grade` must be `0`.
- Offline targets (promote or fire) are updated in the `users` table only if they currently work for the boss's job.

```lua title="client/main.lua"
local ok = ESX.AwaitServerCallback('esx_society:setJob', targetIdentifier, 'taxi', 2, 'promote')

if ok then
    ESX.ShowNotification('Promoted')
end
```

### Reading the balance in a HUD [#reading-the-balance-in-a-hud]

```lua title="client/main.lua"
local job = ESX.PlayerData.job

ESX.TriggerServerCallback('esx_society:getSocietyMoney', function(money)
    print(('Company balance: $%s'):format(money))
end, job.name)
```

## Money washing in detail [#money-washing-in-detail]

1. The boss enters an amount. The server checks boss access and the player's `black_money`.
2. Inside `WithPlayerPersistence`, the amount is removed from `black_money` and a row is written to `society_moneywash_operations` with state `queued` in one transaction. If that fails, the money is refunded.
3. Every day at 03:00 the cron task adds each queued amount to the society account and marks it `paid`.

There is no washing fee: 1,000 black money becomes 1,000 in the society account. Old rows in `society_moneywash` are migrated into the new table on start.

## Integration tips [#integration-tips]

- Create the SQL rows first. A society needs `society_<job>` in `addon_account` (shared = 1). The official job SQL files also add the same name to `datastore` and `addon_inventory`.
- Register the society at the top of your server file, after `esx_society` started. With `ensure` order this is guaranteed.
- To give a second grade boss rights, add its `job_grades.name` to `Config.BossGrades`. The check uses the name, not the grade number.
- Uniforms are stored in `job_grades.skin_male` and `skin_female` and are read by job scripts that dress players from the grade skin.

## Common issues [#common-issues]

- **Menu shows nothing**: the grade name is not in `Config.BossGrades`, or the job name passed to `openBossMenu` differs from the player's job. The server log shows `esx_society: <identifier> attempted open a society boss menu!`.
- **"Society has no shared account"** in the console: the `addon_account` row for `society_<job>` is missing or not marked `shared = 1`.
- **Salary change does nothing**: the value is above `Config.MaxSalary`, not a whole number, equal to the current value, or sent within `Config.JobGradeUpdateCooldown`.
- **Washed money never arrives**: the `cron` resource is not running, the server was offline at 03:00, or the wash module was disabled because a table is not InnoDB. Queued entries stay queued and are paid on the next run.
- **Employee list empty for offline players**: with `Config.EnableESXIdentity = true` the query selects `firstname` and `lastname`. Without esx_identity those columns may not exist; set the option to `false`.

## Related [#related]

- [esx_addonaccount](/docs/addons/esx_addonaccount)
- [esx_datastore](/docs/addons/esx_datastore)
- [Create a job](/docs/guides/create-a-job)
- [Money and accounts](/docs/guides/money-and-accounts)
- [Addons overview](/docs/addons)
