# WP11 — `ProductionOrderController`: four unscoped foreign keys (tenant isolation)

**Repo:** BE (`/home/moonui2/moon-erp-be`) · **Branch:** `hazemdev2` · **Migration:** no · **FE change:** none
**Added mid-run**, from WP9's sweep. Same defect class as WP9, different controller.

## The defect (reported by WP9, independently confirmed by the orchestrator)

`Modules/Production/app/Http/Controllers/ProductionOrderController.php` validates four foreign keys
without scoping them to the caller's company, so a request can reference **another tenant's** record:

| Line | Rule | Method |
|---|---|---|
| 695 | `'warehouse_id' => ['nullable','integer','exists:warehouses,id']` | `issueMaterials()` |
| 703 | `'lines.*.product_id'` | `issueMaterials()` |
| 731 | `'lines.*.unit_id' => ['nullable','integer','exists:units,id']` | `issueMaterials()` |
| 1006 | `'warehouse_id' => ['required','integer','exists:warehouses,id']` | receive-FG |

**Line 695 is the worst, and not because of severity — because of the comment above it.** Line 690
reads:

> `// below so a line can never reference another company's warehouse or business partner.`

and then `$companyId` is resolved on line 691 and used to scope the *per-line* rules — but **not**
this top-level `warehouse_id`. The file documents a guarantee it does not provide. Anyone reading
this code to check the invariant would conclude it holds. (WP9's note: missed by ISS-2026-9166/WP4.)

## Why this is being fixed rather than filed

This run's rule has been *fix what we made worse, report what we merely found* — which is why the
consignment Save-button bug (Deferral D2) was left alone. **Tenant isolation is the exception.**
These are cross-tenant *write* paths, the fix pattern was just landed and proven in `be1f6b4ed`
(WP9), and one of them is protected by a comment that is actively false. Leaving that in place after
finding it is not defensible.

## What to build

Scope all four to `auth()->user()->company_id`, using the **exact idiom already in this file** —
`Rule::exists('…','id')->where('company_id', $companyId)` — the same one the per-line rules a few
lines below already use, and the one WP9 applied in `BomLineRules`. `$companyId` is already resolved
at line 691 in `issueMaterials()`; resolve it the same way in the receive-FG method if it is not.

**Do not** restructure the validation, extract a trait, or touch anything else in these methods. Four
rules, minimal diff.

**Sweep while you are there:** report (do not fix) any other unscoped `exists:` in
`Modules/Production/app/Http/Controllers/`. WP9 already confirmed
`Modules/Production/app/Http/Requests/` is clean, and that
`BackflushFromStagingRequest.php:30` is correctly scoped via `production_order_id` — do not "fix"
that one.

## Acceptance criteria

1. **Red-before-green.** As company A, POST an issue-materials request naming company B's
   `warehouse_id`, `lines.*.product_id`, and `lines.*.unit_id`, and a receive-FG request naming
   company B's `warehouse_id`. All **succeed today**; all must return **422** after. Paste both runs.
2. A legitimate same-company issue and receive-FG still succeed — the existing tests covering these
   two endpoints stay green.
3. The rejection is an ordinary validation message — do not leak that another company's record
   exists.
4. **Fix the comment at line 690** so it describes what the code actually does now (with the fix in
   place it becomes true — verify that rather than assuming).
5. `pest Modules/Production` shows **zero NEW failures**. Current known-good after WP9:
   **640 passed / 10 failed** (ConsignmentFoundation ×1, CostAiAnomalyVariance ×3,
   ProductionVariance ×6).

## 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`.
- 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 duplicate line rules into the shared trait (WP1 and WP9 both flagged it;
both copies are correct today, it is a drift risk not a defect — report, don't do) · unscoped rules
outside `Modules/Production` · any FE change.
