# WP1 — Convert the POS settings tab to the generic, definition-driven pattern

**Repo:** BE `/home/moonui2/moon-erp-be` + FE `/home/moonui2/public_html/moon-erp` · **Branch:** `hazemdev2`
**Migration:** none · **[FIN]:** no

## Goal

Today the **POS tab is the only hand-written settings tab** in the app: three literal `p-select`s bound to three literal keys, plus a Save button. It cannot show a description, cannot render a boolean or an enum, and every future setting would cost four hand edits.

Seven other module tabs (accounting, inventory, products, hrm, sales, purchases, lis) are **generated from `setting_definitions`**: they read the definition's `value_type` and render a switch / number / dropdown, and they display the definition's bilingual `label_*` and `description_*`.

**Make POS the eighth.** After this WP, adding a POS setting must cost exactly: one seeder entry + one reader in code. Zero FE work, and the label + explanation come from the database in both languages.

Do **not** add any new setting definitions in this WP. Only the six that exist today.

## Exact files

### Backend — create
- `Modules/POS/app/Http/Controllers/POSSettingController.php`
- `Modules/POS/app/Http/Requests/UpdatePOSSettingsRequest.php`

### Backend — modify
- `Modules/POS/routes/api.php` — register `GET /pos/settings` and `PUT /pos/settings`
- `Modules/Core/database/seeders/SettingDefinitionSeeder.php` — the POS block starts at **line 2281**; only regroup/relabel if needed (see "Grouping" below). Keep it idempotent (`updateOrCreate`).

### Frontend — modify
- `src/app/features/settings/settings.component.ts`
- `src/app/features/settings/settings.component.html` — the POS tab is at **lines 1017-1066**
- `src/app/core/services/` — add a POS settings service method if one does not exist (check `pos-terminal.service.ts` / create `pos-setting.service.ts`)

## The pattern to mirror — READ THESE FIRST, they are the specification

- `Modules/Sales/app/Http/Controllers/SalesSettingController.php` — copy its exact shape: `HasMiddleware` with `permission:*.settings.view` on `index` and `permission:*.settings.manage` on `update`; `index()` returns `getAllWithDefinitions('pos', $companyId)` mapped to `{definition: SettingDefinitionResource, current_value}`; `update()` loops `$validated` and calls `settingsService->set("pos.{$key}", …)`, then returns all current settings with the module prefix stripped.
- `Modules/Sales/app/Http/Requests/UpdateSalesSettingsRequest.php` — **must** `use MergesSettingDefinitionRules` and return `$this->withDefinitionRules('pos', [...explicit rules...])`. This is what makes future settings save without touching the request again. Put explicit rich rules only for the account/warehouse/customer FKs.
- FE: `settings.component.ts` `loadSalesSettings()` (~line 627), `getSalesGroupSettings()`, `updateSalesValue()`, `getSalesEnumOptions()`, `saveSalesSettings()`, and the `salesGroupMap` at line 155. FE template: `settings.component.html` lines **327-430** — the `@switch (setting.value_type)` block.

## Permissions — already done, do not invent

`pos.settings.view` and `pos.settings.manage` already exist in `Modules/Core/app/Support/PermissionCatalog.php` and are granted in `RolePermissionSeeder.php` (~837-838). `SettingDefinitionSeeder::MODULE_SETTING_PERMISSIONS['pos']` already maps to `pos.settings.manage`. **No permission work in this WP.**

## The six existing keys

`pos.default_customer_id` (integer) · `pos.default_warehouse_id` (integer) · `pos.cash_receiving_account_id` (integer) · `pos.card_receiving_account_id` (integer) · `pos.default_receiving_account_id` (integer) · `pos.default_receiving_account_type` (enum: `petty_cash|bank_account`).

All six already carry `label_en`/`label_ar`/`description_en`/`description_ar` and `display_group = 'pos_general'`.

## Grouping

The Sales tab groups settings visually. Introduce POS groups now so later settings land correctly. Use `display_group` on the definition as the source, and a `posGroupMap` in the FE only as an override, exactly as Sales does (`salesGroupMap[key] || 'general'` — **an unmapped key must still render**, in a "general" bucket; never hide it).

Suggested groups for the six existing keys: keep `pos_general` for defaults (customer, warehouse) and introduce `pos_accounts` for the four GL/receiving-account keys. Update the seeder entries' `display_group` accordingly and give sensible `display_order`.

## Entity pickers

Sales solves "this integer is really a foreign key" with suffix heuristics: `isSalesAccountSetting` (`_account_id`), `isSalesWarehouseSetting` (`_warehouse_id`), `isSalesTaxSetting` (`_tax_id`), `isSalesCustomerSetting` (exact key). Do the same for POS so `default_customer_id` renders a customer picker, `default_warehouse_id` a warehouse picker, and the three `_account_id` keys account pickers — **preserving today's behaviour**, which already shows real pickers for these three.

`default_receiving_account_type` is an `enum` and must render as a dropdown from `allowed_values`.

## Acceptance criteria

1. `GET /pos/settings` returns all six definitions + current values, gated by `pos.settings.view`; a user without it gets 403.
2. `PUT /pos/settings` persists via `SettingsService::set('pos.'.$key, …)`, gated by `pos.settings.manage`.
3. `UpdatePOSSettingsRequest` uses `MergesSettingDefinitionRules` — **prove it**: a definition with no explicit rule still saves.
4. The FE POS tab renders from definitions: each setting shows its **label and its description**, in the current language, and the control type follows `value_type`.
5. The three settings that render as pickers today (customer / warehouse / account) still render as pickers and still save the same values. **No behavioural regression for an existing user.**
6. An unmapped/new key renders in a general group rather than disappearing.
7. `ng build` green.

## Tests

- New: `Modules/POS/tests/Feature/POSSettingApiTest.php`. Cover: view permission gate (403), manage permission gate (403), index shape (`data[].definition` + `data[].current_value`), update persists and round-trips, and **a key with no explicit rule still saves** (the `MergesSettingDefinitionRules` guarantee).
- Model the test on `Modules/Core/tests/Feature/SettingApiTest.php` and `SettingPermissionEnforcementTest.php`.
- ⚠️ **Pest loads every test file into ONE process.** Any top-level helper function you declare must be prefixed with this file's subject (e.g. `posSettingActor()`), never a generic name. A duplicate top-level function is a fatal redeclare that kills the WHOLE suite with exit 255 and zero output. This has now happened **four times** in this project — most recently `makeOrder()` in WebStore vs Production, which I fixed today.

## Out of scope

- Do NOT add new setting definitions (pharmacy toggles come later, each with its own reader).
- Do NOT touch the terminal-settings screen `src/app/features/pos/pos-settings/` — that is WP2/WP8's area.
- Do NOT touch any other module's settings tab.
- Do NOT modify `src/assets/i18n/{ar,en}.json` beyond adding needed keys — and **never** run `git checkout`/`restore`/`stash` on those two files.

## Environment

- Run tests with the explicit CLI php: `/opt/cpanel/ea-php82/root/usr/bin/php -d memory_limit=1G vendor/bin/pest --filter='POSSettingApiTest'` (the default `php` is php-cgi and dies with "Undefined constant STDOUT").
- Format only files you touched: `./vendor/bin/pint <paths>`.
- After editing any file as root: `chown moonui2:moonui2 <file>`.
- The working tree is **not clean** (POS overhaul work pending the owner's push). Do not revert, stash, or commit anything you did not write.
- FE build: `cd /home/moonui2/public_html/moon-erp && npx ng build --base-href /app/`.
