# C1 — Activate `inventory.require_batch_on_receipt`  **[FIN-adjacent]**

**Phase C, group «الصلاحية والتشغيلات», package 1 of 4**
**Repo:** BE (+ FE only if the receipt dialog needs a required-marker) · **Branch:** `hazemdev2` · **Migration:** no

## Why this one is first

Expiry and batch are captured at exactly one moment: **keeper approval of an inventory receipt**. If they are not captured there, no downstream screen can ever recover them — not the till, not FEFO, not the expiring-lots report. Every other setting in this group depends on the data this one guarantees.

## The defect

`Modules/Inventory/app/Services/ReceiptLotService.php` (~line 45):

```php
if (empty($item['batches'])) {
    continue;   // ← a BATCH-TRACKED product can be received with no batches at all
}
```

So a product whose `tracking_type = batch` can be received with **zero** batch rows: no lot row is ever created, and the goods enter `inventory_stock_balances` with no batch and no expiry. `ApproveInventoryReceiptRequest` compounds it — `expiry_date` is `['nullable','date']`, and the only cross-check is `production_date < expiry_date`.

There is also **no guard against receiving already-expired goods** (no `after:today` anywhere; `ApproveReceipt` carries the date straight through).

## What to build

**The reader for `inventory.require_batch_on_receipt`** (already seeded, currently locked).

When the setting is ON, approving an inventory receipt must **refuse** (422) if any line whose product is `tracking_type = batch` has:
- no batch rows at all, **or**
- a batch row missing `batch_number`, **or**
- a batch row missing `expiry_date`, **or**
- batch quantities that do not sum to the line quantity (this rule already exists — keep it, do not duplicate it).

When OFF: **byte-for-byte today's behaviour.** That is the regression guard.

Also add, gated by the same setting: refuse a batch whose `expiry_date` is already in the past at receipt time. Message must say which line and which batch.

### Unlocking the setting
Flip `is_implemented => true` for `inventory.require_batch_on_receipt` in `Modules/Core/database/seeders/SettingDefinitionSeeder.php` **in this same WP** — that is the activation contract recorded in the seeder's header comment. Re-seed on dev.

⚠️ **Read the `⚠ TRAP` block in that seeder first.** `POSSettingController::update()` writes `set("pos.{$key}")`, re-prefixing whatever short key the tab posts. This setting is `inventory.*` and is rendered on the POS tab, so **a write from that tab would land on `pos.inventory.require_batch_on_receipt`**. Fix the controller's key resolution **in this WP** — resolve the full key from the definition rather than assuming the `pos.` prefix. That trap is why it was written down.

### Default
Seeded default is ON (`true`). **Verify that against existing data before you keep it**: if any company on dev has batch-tracked products currently received without batches, an ON default turns a working flow into a 422 on their next receipt. Report what you find; if the data says so, ship the default OFF and say why.

## Exact files

- `Modules/Inventory/app/Services/ReceiptLotService.php` — the guard
- `Modules/Inventory/app/Http/Requests/ApproveInventoryReceiptRequest.php` — where a validation-shaped refusal belongs, if that is the better seam; decide by reading both and say which you chose and why
- `Modules/Inventory/app/Actions/ApproveReceipt.php` — only if the refusal must happen inside the transaction
- `Modules/Core/database/seeders/SettingDefinitionSeeder.php` — unlock
- `Modules/POS/app/Http/Controllers/POSSettingController.php` — the key-prefix trap
- `Modules/Inventory/lang/{en,ar}/…` — the messages, bilingual
- FE: only if the receipt-approval dialog needs a required marker on batch/expiry when the setting is on. If you touch FE, build and deploy.

## Acceptance criteria

1. Setting ON + batch-tracked line with no batches → **422** naming the line. Setting OFF → posts exactly as today.
2. ON + batch row missing `batch_number` → 422. ON + missing `expiry_date` → 422.
3. ON + `expiry_date` in the past → 422 naming line and batch.
4. ON + a **non**-batch-tracked product with no batches → **posts normally** (the rule must not leak to `tracking_type = none`).
5. ON + a complete batch row → posts, and the lot row is created as today.
6. **Red-before-green for criteria 1–3**: run each test against current code, show it wrongly succeeds, then implement. Paste both runs.
7. `inventory.require_batch_on_receipt` reads `is_implemented = true` after re-seed, and a `PUT` from the settings screen now **saves** it (it 422'd while locked) **and lands on the correct key** — assert the stored `setting_key` is `inventory.require_batch_on_receipt`, NOT `pos.inventory.…`.
8. Every other setting stays locked (41 remain).

## Tests

New `Modules/Inventory/tests/Feature/RequireBatchOnReceiptTest.php`.
⚠️ **Pest loads every test file into ONE process** — prefix every top-level helper with its file's subject. A generic name is a fatal redeclare that kills the whole suite: exit 255, zero output. **Five occurrences in this project.**

## Baselines — green = zero NEW failures
- `pest Modules/Inventory` — **establish and report the baseline first**, before your change; there is no recorded one.
- `pest --filter='ProductApiTest|ProductUnitApiTest|SettingApiTest|SettingPermissionEnforcementTest|BrandApiTest|UnitConversionServiceTest|UnitBaseValidationTest|POSTerminalApiTest|ManufacturerLookupApiTest'` → **210 passed / 14 failed**
- `pest Modules/POS` → **236 passed / 0 failed**
- `pest Modules/Core` → **696 passed / 10 failed**

## Environment
- BE `/home/moonui2/moon-erp-be`, FE `/home/moonui2/public_html/moon-erp`, branch `hazemdev2`. **moonui2 ONLY — never `/home/moonui`.**
- BE tests: `/opt/cpanel/ea-php82/root/usr/bin/php -d memory_limit=1G vendor/bin/pest --filter='…'` (default `php` is php-cgi → "Undefined constant STDOUT").
- Re-seed on dev: `artisan db:seed --class="Modules\\Core\\Database\\Seeders\\SettingDefinitionSeeder" --force`, then `bash local-deploy.sh`. ⛔ Never `migrate:fresh`/`refresh`/`db:wipe` on `moonui2_dev_be`.
- If FE changes: `npx ng build --base-href /app/`, then deploy to `/app` (clean `*.js|css|html|ico`, `\cp -rf dist/moon-erp/browser/*`, verify `assets/config.json` apiUrl, `chown -R moonui2:moonui2`). Dev deploy only — no ship, no merge, no push, **no commit**.
- `pint` on touched files only; `chown moonui2:moonui2` after every edit.
- Dirty working tree by design — never revert/stash/commit anything you did not write.
- Never `git checkout`/`restore`/`stash` on `src/assets/i18n/{ar,en}.json`.

## Out of scope
- The lot-shortfall fix (C2), the POS expiry guard (C3), the override enforcement (C4).
- Any other locked setting.
- Do not touch `Modules/POS` beyond the key-prefix trap fix.
