# WP4 — A separate POS sale permission  **[SECURITY]**

**Repo:** BE `/home/moonui2/moon-erp-be` · **Branch:** `hazemdev2` · **Migration:** **YES (bridge)**

## Goal

`POSSaleController::middleware()` gates the till's sale endpoint with **`permission:sales.invoices.create`**. So anyone who can raise a back-office sales invoice can post a till sale on any terminal — and conversely a cashier must be granted a back-office invoice permission to do their job. The two are not the same authority.

Introduce **`pos.sales.create`** and gate the POS sale endpoint with it.

## Exact files

- `Modules/POS/app/Http/Controllers/POSSaleController.php` — swap the middleware.
- `Modules/Core/app/Support/PermissionCatalog.php` — declare the new permission alongside the existing `pos.*` entries (`pos.settings.view`, `pos.settings.manage`, `pos.refunds.create` are already there — follow their exact shape and placement).
- `Modules/Core/database/seeders/RolePermissionSeeder.php` — add to the master list and grant it to the same roles that hold `pos.refunds.create` / the POS role set today.
- **New bridge migration** under `Modules/POS/database/migrations/` — timestamped AFTER every existing migration.

## ⚠️ The bridge migration is the whole point — do not skip it

`RolePermissionSeeder` is **deliberately excluded** from the MoonStack updater's seeder list. The updater runs `migrate`, not the seeders. A permission added only to the seeder is therefore **dead on 100% of the existing fleet** — every client's cashier would get a 403 and the till would stop selling.

This exact trap was caught in the POS overhaul's seam review for `pos.refunds.create`. **Find that migration and copy it** — search `Modules/POS/database/migrations/` for the one that bridges `pos.refunds.create` (around the `620000` series). Mirror it exactly: create the permission if missing, grant it to the roles that already hold the equivalent authority, idempotent, forward-only, non-fatal.

**Grant rule for existing installs:** every role that currently holds `sales.invoices.create` must receive `pos.sales.create`, so no existing user loses the ability to sell the moment this ships. That is the migration's job, and it is what makes the change non-breaking.

## Acceptance criteria

1. A user with `pos.sales.create` but **without** `sales.invoices.create` can post a POS sale.
2. A user with `sales.invoices.create` but **without** `pos.sales.create` gets **403** on `POST /pos/sales`.
3. Running the bridge migration on a database whose roles hold `sales.invoices.create` grants them `pos.sales.create`; running it twice changes nothing (idempotent).
4. The permission appears in `PermissionCatalog` so it is visible in the roles UI.
5. No other POS endpoint's gate changes in this WP.

## Tests

New or extended feature test proving (1) and (2) — both directions, explicitly. Model on the existing POS permission tests and on whatever test covers `pos.refunds.create`.

Also assert the catalog contains the key (there is a `PermissionCatalogTest` in `Modules/Core/tests/Feature/`).

⚠️ Pest loads every test file into ONE process — prefix any top-level helper with its file's subject. A generic name is a fatal redeclare that kills the entire suite (exit 255, zero output). Fourth occurrence fixed today.

## Migration execution

Run the new migration on the dev DB **in the same step the code lands** (`php artisan migrate --force` via the CLI php) — the dev backend runs live, so code without its migration means errors for anyone using dev.
⛔ Never `migrate:fresh`, `migrate:refresh`, `db:wipe`, or `RefreshDatabase` against `moonui2_dev_be` — it is not binlogged.

## Out of scope

- Do not change any other controller's permissions.
- Do not touch the refund, session, or product POS endpoints.
- Do not add setting definitions.

## Environment

- BE tests: `/opt/cpanel/ea-php82/root/usr/bin/php -d memory_limit=1G vendor/bin/pest --filter='<TestName>'`.
- Baseline: 174 passed / 14 pre-existing failures (all `StorefrontProductApiTest`). Green = no NEW failures.
- `./vendor/bin/pint` on touched files only. `chown moonui2:moonui2` after every edit.
- Working tree is deliberately dirty — never revert/stash/commit anything you did not write. Do not commit; the orchestrator does.
