---
name: setting-definition-schema
description: The SHAPE of a setting definition + the complete catalog of every default-account (*_account_id) setting in Core's SettingDefinitionSeeder.php, how account-typed settings are marked, and where module-specific account defs also live. Foundation for the "Default Accounts /setup page" extension study.
updated: 2026-06-18
---

# Setting-Definition Schema & Default-Account Catalog (Core)

This is the **foundation topic** for the "extend the /setup Default-Accounts page" study.
It documents (a) the exact shape of a setting definition, (b) how an account-typed setting
is recognised (there is **no dedicated `account` value type** — it is convention by key
suffix + `value_type=integer`), and (c) the **complete catalog of every `*_account_id`
definition** in the canonical Core seeder, plus a pointer to the per-module seeders where
LIS/Production/HR-style account settings also live.

> Sibling topics (written by parallel agents) cover the LAB, MANUFACTURING and HR areas in
> depth. **This topic is the schema/Core layer they all build on.**

---

## 1. Where setting definitions live

The canonical, system-wide ("global") definitions are seeded by:

| File | Role |
|------|------|
| `Modules/Core/database/seeders/SettingDefinitionSeeder.php` | **Canonical / largest** definition list (165 defs total). Inserted via `SettingDefinition::updateOrCreate(['setting_key'=>…], $def)` — `SettingDefinitionSeeder.php:14-19`. |
| `Modules/LIS/database/seeders/LabSettingDefinitionSeeder.php` | LIS-specific defs — **10 extra `*_account_id`** beyond the 4 `lis.*` in Core. |
| `Modules/Production/database/seeders/ProductionSettingDefinitionSeeder.php` | Production/MFG defs — **8 `*_account_id`** (Core has **0** production account defs). |
| `Modules/Production/database/seeders/ProductionAlertSettingDefinitionSeeder.php` | Production alert thresholds (no account defs of interest). |
| `Modules/Inventory/database/seeders/InventorySettingDefinitionSeeder.php` | Inventory defs — **0 `*_account_id`** (inventory posting uses sales/purchases inventory accounts). |

> **KEY TAKEAWAY for the /setup extension:** account settings are spread across **Core +
> 2 module seeders**. The Core seeder alone is NOT the full picture for LAB and MFG.

The model + table:
- Model `Modules/Core/app/Models/SettingDefinition.php` → table `setting_definitions`.
- Per-company chosen values are stored separately in the `settings` table via
  `Modules\Core\Models\Setting` (key/value/company_id/branch_id/user_id).

---

## 2. The shape of a setting definition

Every definition is an associative array. The model's `$fillable`
(`SettingDefinition.php:14-29`) is the authoritative key list:

| Array key | Type | Meaning | Example (account setting) |
|-----------|------|---------|---------------------------|
| `setting_key` | string (unique) | dotted key `module.name`; account keys end in **`_account_id`** | `sales.revenue_account_id` |
| `module` | string | owning module | `sales`, `accounting`, `hrm`, `lis`, `pos`, `purchases`, `core`, `inventory` |
| `value_type` | enum string | one of the `SettingValueType` cases (below) | `integer` (always, for accounts) |
| `default_value` | mixed/null | seeded default | `null` (accounts ship empty) |
| `allowed_values` | array/null | enum options; cast to `array` | `null` (accounts have none) |
| `scope` | enum string | `SettingScope` (company/branch/user) | `company` (all account settings) |
| `label_ar` / `label_en` | string | bilingual UI label | `حساب الإيرادات` / `Revenue GL Account` |
| `description_ar` / `description_en` | string/null | bilingual help text | see catalog below |
| `display_group` | string | UI grouping key (drives Settings-page sections) | `sales`, `hrm_accounting`, `lis_accounting`, `pos_general`, `accounting` |
| `display_order` | int | order within the group | `15` |
| `is_visible` | bool | show in Settings UI | `true` |
| `requires_restart` | bool | needs app restart | `false` |

### `SettingValueType` enum — `Modules/Core/app/Enums/SettingValueType.php`
```
String  = 'string'
Integer = 'integer'   ← account settings use THIS
Decimal = 'decimal'
Boolean = 'boolean'
Json    = 'json'
Enum    = 'enum'
```

### 🔑 How an account-typed setting is marked
There is **NO `account` value type and NO special input_type / options source** in the
schema. An "account" setting is identified purely by **convention**:

1. `value_type => 'integer'` (it stores a GL account **id**), and
2. the `setting_key` **ends in `_account_id`**, and
3. `default_value => null`, `allowed_values => null`, `scope => 'company'`.

The stored value is the **`accounts.id`** (integer PK) of a row in the company's chart of
accounts (`Modules\Accounting\Models\Account`). There is no FK constraint and no UI hint in
the definition telling the frontend "render an account picker" — the FE must infer that from
the `_account_id` suffix (this is exactly what the /setup page does today, by hardcoding the
list of keys rather than reading a flag). **If the /setup extension wants to auto-discover
account settings, the only signal available is the key suffix.**

Value-type distribution across the Core seeder (for reference):
`integer` ×68, `boolean` ×48, `enum` ×15, `string` ×15, `json` ×4, `decimal` ×1.
(Not every `integer` is an account — e.g. `*_cost_center_id`, sequence ids, day counts. Only
the **33** ending in `_account_id` are accounts; see §3.)

---

## 3. Complete catalog — EVERY `*_account_id` definition in the Core seeder

All 33 entries below are `value_type=integer, default_value=null, allowed_values=null,
scope=company, is_visible=true, requires_restart=false`. Sorted by file position.
`display_group` is what the Settings UI uses to bucket them.

### Accounting (`module=accounting`, `display_group=accounting`)
| # | line | setting_key | label_en | label_ar | purpose |
|---|------|-------------|----------|----------|---------|
| 1 | 309 | `accounting.checks_issued_account_id` | Checks Issued Intermediate Account | حساب الشيكات المُصدَرة الوسيط | Liability credited on check issue, debited when check clears (≈2103) |
| 2 | 325 | `accounting.checks_received_account_id` | Checks Received Intermediate Account | حساب الشيكات المُستلَمة الوسيط | Asset debited on check receipt, credited when bank collects (≈1103) |
| 3 | 341 | `accounting.zakat.expense_account_id` | Zakat Expense Account | حساب مصروف الزكاة | Debited when a zakat declaration is approved (P&L) |
| 4 | 357 | `accounting.zakat.payable_account_id` | Zakat Payable Account | حساب الزكاة المستحقة | Credited on approval, debited on payment (liability) |
| 5 | 373 | `accounting.wht_payable_account_id` | Withholding Tax Payable Account | حساب ضريبة الاستقطاع المستحقة | Credited when a payment voucher carries WHT (KSA WHT→ZATCA) |

### Sales (`module=sales`, `display_group=sales`)
| # | line | setting_key | label_en | label_ar | purpose |
|---|------|-------------|----------|----------|---------|
| 6 | 663 | `sales.revenue_account_id` | Revenue GL Account | حساب الإيرادات | Revenue side of sales-invoice JE |
| 7 | 679 | `sales.cogs_account_id` | COGS GL Account | حساب تكلفة البضاعة المباعة | Cost of Goods Sold on sales invoice |
| 8 | 695 | `sales.receivable_account_id` | Accounts Receivable GL Account | حساب المدينين | AR for sales invoices |
| 9 | 711 | `sales.inventory_account_id` | Inventory GL Account | حساب المخزون | Inventory (credit) side of COGS JE on sales invoices |
| 10 | 743 | `sales.tax_payable_account_id` | Tax Payable GL Account | حساب الضريبة المستحقة | Output VAT on sales invoices |
| 11 | 759 | `sales.discount_account_id` | Sales Discount GL Account | حساب خصم المبيعات | Sales discount JE |

> Companion (NOT an account, but governs which AR account is used): `sales.use_customer_ar_account` (boolean, line 727) — when true, use the customer's AR sub-account instead of `sales.receivable_account_id`. Mirror exists in purchases: `purchases.use_supplier_ap_account` (line 1163).

### Purchases (`module=purchases`, `display_group=purchases`)
| # | line | setting_key | label_en | label_ar | purpose |
|---|------|-------------|----------|----------|---------|
| 12 | 1084 | `purchases.inventory_account_id` | Inventory GL Account | حساب المخزون | Inventory (debit) on purchase JE |
| 13 | 1100 | `purchases.expense_account_id` | Expense GL Account | حساب المصروفات | Default expense account for purchases |
| 14 | 1116 | `purchases.payable_account_id` | Accounts Payable GL Account | حساب الدائنين | AP for purchase invoices |
| 15 | 1132 | `purchases.tax_receivable_account_id` | Input Tax GL Account | حساب ضريبة المدخلات | Input VAT (receivable) |
| 16 | 1148 | `purchases.discount_account_id` | Purchase Discount GL Account | حساب خصم المشتريات | Purchase discount JE |

### HRM (`module=hrm`, `display_group=hrm_accounting`)
| # | line | setting_key | label_en | label_ar | purpose |
|---|------|-------------|----------|----------|---------|
| 17 | 1443 | `hrm.salary_expense_account_id` | Salary Expense Account | حساب مصروف الرواتب | Salary expense in payroll JE |
| 18 | 1459 | `hrm.salary_payable_account_id` | Salary Payable Account | حساب الرواتب المستحقة | Salary payable (liability) in payroll JE |
| 19 | 1475 | `hrm.social_insurance_account_id` | Social Insurance Account | حساب التأمينات الاجتماعية | Social-insurance deductions |
| 20 | 1491 | `hrm.medical_insurance_account_id` | Medical Insurance Account | حساب التأمين الطبي | Medical-insurance deductions |
| 21 | 1507 | `hrm.income_tax_account_id` | Income Tax Account | حساب ضريبة الدخل | Income-tax withholding |
| 22 | 1523 | `hrm.loan_receivable_account_id` | Loan Receivable Account | حساب سلف الموظفين | Employee-loan receivables |
| 23 | 1539 | `hrm.eos_provision_account_id` | End of Service Provision Account | حساب مخصص نهاية الخدمة | EOS provision (liability) |
| 24 | 1555 | `hrm.eos_expense_account_id` | End of Service Expense Account | حساب مصروف نهاية الخدمة | EOS expense |
| 25 | 1711 | `hrm.overtime_expense_account_id` | Overtime Expense Account | حساب مصروف الأوفرتايم | Overtime expense (one-line compact def) |
| 26 | 1712 | `hrm.leave_accrual_account_id` | Leave Accrual Account | حساب استحقاق الإجازات | Leave-accrual liability (one-line compact def) |

### LIS / Lab (`module=lis`, `display_group=lis_accounting`)
| # | line | setting_key | label_en | label_ar | purpose |
|---|------|-------------|----------|----------|---------|
| 27 | 1716 | `lis.receivable_account_id` | Lab Receivable GL Account | حساب مدينين المختبر | AR for lab invoices ⚠️ must be a **detail leaf**, not the AR parent (see lis-receivable-header-account-trap) |
| 28 | 1732 | `lis.revenue_account_id` | Lab Revenue GL Account | حساب إيرادات المختبر | Revenue for lab invoices |
| 29 | 1748 | `lis.insurance_receivable_account_id` | Insurance Receivable GL Account | حساب مدينين التأمين | Insurance-company receivables |
| 30 | 1764 | `lis.tax_payable_account_id` | Lab Tax Payable GL Account | حساب ضريبة المختبر المستحقة | Output VAT for lab invoices |

> ⚠️ The 4 `lis.*` above are only the ones defined in **Core**. The LIS module's own
> `LabSettingDefinitionSeeder.php` defines **10 more** `*_account_id` keys (payment-method
> accounts etc.) — documented in the LAB sibling topic. Don't assume Core = all of LIS.

### POS (`module=pos`, `display_group=pos_general`)
| # | line | setting_key | label_en | label_ar | purpose |
|---|------|-------------|----------|----------|---------|
| 31 | 1974 | `pos.cash_receiving_account_id` | Cash Receiving Account | حساب استلام الكاش (الخزينة) | Receives cash POS payments (treasury) |
| 32 | 1990 | `pos.card_receiving_account_id` | Card/Network Receiving Account | حساب استلام الشبكة (حساب وسيط) | Intermediary for card/network POS payments |
| 33 | 2006 | `pos.default_receiving_account_id` | Default Receiving Account | حساب الاستلام العام | Fallback when no method-specific account configured |

> Companion (NOT an account): `pos.default_receiving_account_type` (enum `petty_cash|bank_account`, line 2022).

**Production / MFG: ZERO `*_account_id` defs in the Core seeder.** All MFG default accounts
live in `Modules/Production/database/seeders/ProductionSettingDefinitionSeeder.php` (8 keys) —
see the MANUFACTURING sibling topic.

---

## 4. Auto-seeding defaults: `seedDefaultAccountSettings()`

`SettingDefinitionSeeder::seedDefaultAccountSettings(int $companyId)` —
`SettingDefinitionSeeder.php:2111-2164` — is a **static helper** (called after a company's
chart of accounts is seeded). It maps a small subset of account settings to **account
codes**, resolves each code→id for that company, and writes the `settings` row **only if not
already set** (`SettingDefinitionSeeder.php:2113-2123`):

| setting_key | seeded account code |
|-------------|--------------------|
| `sales.revenue_account_id` | `4101` |
| `sales.cogs_account_id` | `5101` |
| `sales.receivable_account_id` | `1103` |
| `sales.inventory_account_id` | `1105` |
| `sales.discount_account_id` | `4105` |
| `purchases.inventory_account_id` | `1105` |
| `purchases.payable_account_id` | `2101` |

> Only **7** of the 33 account settings get an auto-default. **HRM, LIS, POS, accounting
> (zakat/wht/checks) and all of Production ship NULL** and must be set manually — which is
> exactly the gap the /setup-page extension is meant to close. After writing, the helper
> busts the cache: `Cache::forget("settings.company.{$companyId}")` (line 2161).

---

## 5. ⚠️ The /setup page uses DIFFERENT keys (gap to reconcile before extending)

The current first-run `/setup` Default-Accounts step
(`/home/moonui/public_html/moon-erp/src/app/features/setup/setup-wizard.component.ts`,
`saveDefaults()` at lines 674-739) writes **7 settings whose keys do NOT exist in any
SettingDefinitionSeeder** — they are free-form `settings` rows with no backing definition:

| /setup key (FE, line) | conceptual meaning | matching *defined* account setting? |
|-----------------------|--------------------|-------------------------------------|
| `accounting.default_cash_account` (680) | cash (الخزينة) | ❌ no def; cash lives as treasuries / `pos.cash_receiving_account_id` |
| `accounting.default_bank_account` (685) | bank | ❌ no def |
| `accounting.default_client_account` (690) | receivables (المدينون) | ❌ no def (≠ `sales.receivable_account_id`) |
| `accounting.default_supplier_account` (695) | payables (الدائنون) | ❌ no def (≠ `purchases.payable_account_id`) |
| `accounting.default_expense_account` (700) | expenses | ❌ no def |
| `accounting.default_revenue_account` (705) | revenue | ❌ no def (≠ `sales.revenue_account_id`) |
| `accounting.default_check_account` (710) | notes/check (default code `1104`) | ❌ no def (≠ `accounting.checks_*_account_id`) |

Findings for the extension work:
- These 7 keys are **undefined** (no `setting_definitions` row) and only **one** is read by
  any backend code: `DoctorCommissionService.php:385` reads
  **`accounting.default_cash_account_id`** (note the trailing `_id`!) — which is a **third,
  different key** the FE doesn't even write. So today the /setup "default accounts" are
  largely **write-only / cosmetic** and don't line up with the real posting keys in §3.
- **Recommendation context (not a change):** the extension should surface the **real,
  defined `*_account_id` keys from §3 (+ the LIS/Production module seeders)** rather than the
  legacy `accounting.default_*_account` keys, OR a definitive mapping must be agreed. This
  divergence is the single most important thing to resolve before adding LAB/MFG/HR sections.

---

## 6. Consumption (where the value is used when posting)

The definitions themselves are inert; the **id is read at posting time** via the settings
service and dropped into journal-entry lines. The Core seeder does not post — consumers live
in each module's invoice/payroll/posting services (documented per-area in the sibling
topics). General pattern:
```php
$accountId = $this->settingsService->get('sales.receivable_account_id', $companyId);
// → used as the GL account_id on a JournalEntryLine when the document posts.
```
A concrete cross-module example already in code:
`Modules/LIS/app/Services/DoctorCommissionService.php:385` falls back to
`accounting.default_cash_account_id` for the cash side of a commission payout.

---

## 7. Quick reference — "is this setting a default account?"
```
value_type === 'integer'  &&  endsWith(setting_key, '_account_id')
   ⇒ stored value is an accounts.id (GL account) in the company CoA
```
No other flag exists. Account count: **33 in Core** + **8 in Production** + **10 in LIS** +
**0 in Inventory** = the full default-account surface to consider for the /setup extension.
