# WP9 — BOM line rules must be company-scoped (tenant isolation)

**Repo:** BE (`/home/moonui2/moon-erp-be`) · **Branch:** `hazemdev2` · **Migration:** no · **FE change:** none
**Added mid-run**, from a finding WP1 surfaced. Not in the original analysis.

## The defect (verified by the orchestrator, do not re-derive)

`Modules/Production/app/Http/Requests/Concerns/BomLineRules.php` — created by WP1 and now shared by
`UpdateBomRequest`, `BomController::syncComponents()` and `BomController::syncOperations()` — leaves
three foreign keys **unscoped**:

```php
line 31:  'components.*.product_id'            => ['required', 'exists:products,id'],
line 34:  'components.*.unit_id'               => ['required', 'exists:units,id'],
line 63:  'operations.*.production_center_id'  => ['required', 'exists:production_centers,id'],
```

A caller can therefore attach **another tenant's** product, unit, or production centre to their own
BOM.

Two facts make this unambiguous rather than a judgement call:

1. **The substitute product on the very next lines IS scoped** —
   `line 50: Rule::exists('products','id')->where('company_id', auth()->user()?->company_id)`.
2. **`StoreBomRequest` (create) scopes all of them** — `StoreBomRequest.php:45,48` use
   `Rule::exists(...)->where('company_id', $companyId)`.

So creating a BOM is protected and editing one is not, inside the same feature. That is a defect, not
a deliberate asymmetry.

**Pre-existing, but newly widened.** The hole was always reachable through
`PUT /boms/{bom}/components`. WP1 (`513cc5697`) made the same rules govern `PUT /boms/{bom}` — the
path the ordinary Save button uses — so it is now reachable from normal editing. WP1 deliberately
left it because its own acceptance criteria required the sync endpoints' behaviour stay identical.
That constraint does not apply to this WP.

## What to build

Scope all three to the authenticated user's company, matching the pattern `StoreBomRequest` and the
substitute rule already use. Keep the rules in the **one** shared trait — do not fork a second copy.

Read `StoreBomRequest.php` first and mirror its exact idiom, including how it resolves the company id,
so the codebase keeps one way of expressing this.

**Check while you are there:** are there other unscoped `exists:` rules on the same trait or on the
Production BOM/routing request classes? If yes, list them in your report. Fix only the three named
above plus any that are unambiguously the same defect in the same trait; anything wider gets reported,
not silently changed.

## Acceptance criteria

1. **Red-before-green.** A test that, as company A, `PUT`s a BOM with a `product_id` belonging to
   company B gets **422** after the change and **succeeds today**. Paste both runs. Do the same for
   `unit_id` and `operations.*.production_center_id`.
2. The same protection holds through **both** entry points: `PUT /boms/{bom}` and
   `PUT /boms/{bom}/components` (and `/operations` for the production centre).
3. A legitimate same-company save is unaffected — WP1's `ProductionBomUpdateLinesTest` stays green
   (6 passed / 36 assertions).
4. `pest Modules/Production` shows **zero NEW failures** vs the WP1 baseline: **622 passed / 10
   failed**, the 10 being ConsignmentFoundation ×1, CostAiAnomalyVariance ×3, ProductionVariance ×6.
5. The error message a user sees is the ordinary validation message — do **not** leak the existence
   of another company's record.

## Environment / rules

- Tests: `cd /home/moonui2/moon-erp-be && /opt/cpanel/ea-php82/root/usr/bin/php -d memory_limit=1G vendor/bin/pest --filter='…'`
  (bare `php` is php-cgi → "Undefined constant STDOUT").
- Scoped tests only: your new file + `pest Modules/Production`.
- ⛔ **Pest loads every test file into ONE process** — prefix every top-level helper with its file's
  subject. Duplicate top-level function = fatal redeclare, exit 255, zero output (5 occurrences here).
- ⛔ NEVER `migrate:fresh` / `migrate:refresh` / `db:wipe` on `moonui2_dev_be` — not binlogged.
- API auth header is `X-Authorization: Bearer` (not `Authorization`).
- `./vendor/bin/pint` on touched files only. `chown moonui2:moonui2` every edited file.
  `bash local-deploy.sh` after BE edits.
- Add a bilingual `[Unreleased]` bullet to `docs/moonstack/CHANGELOG.md`
  (`- **Headline.** English. {{ar}} **عنوان.** عربي.`) — this is a user-facing security fix.
- Commit on `hazemdev2`, conventional (`fix(production): …`). **Do not push, do not merge.**
- moonui2 ONLY — never `/home/moonui`. Never print a git remote URL.

## Out of scope

Folding `StoreBomRequest`'s stricter copy into the shared trait (WP1 flagged it; it is a tidiness
change, not a defect — report it, don't do it) · unscoped `exists:` rules in other modules · any FE
change.
