# WP1 — StockService conversion capability [FIN] (dormant until WP2)

## Goal
Give `StockService` the CAPABILITY to convert a line quantity (in its entered unit) to the product's BASE unit, using WP0's `UnitConversionService`. **This WP wires NO callers** — it only adds an optional `unit_id` input to the six quantity-taking methods + the inverse unit_cost handling + a `base_quantity` column. Because no existing caller passes `unit_id`, behaviour is **byte-for-byte unchanged** (identity) until WP2 activates it. The Inventory suite (139/0) must stay green with zero test edits.

## READ FIRST
- `/home/moonui2/moon-erp-be/Modules/Inventory/app/Services/StockService.php` — study the `$data` array shape each method takes, and how it writes movements / cost layers / balances. Methods: `increaseStock` (~:40, qty ~:51), `decreaseStock` (~:125, qty ~:134), `getIssueCost` (cost read; FIFO consume ~:278; WAC), `reserve`/`release` (~:351-381), `availableQuantity` (~:387-400), `getOrCreateBalance` (~:424), FIFO layer write (~:74).
- `/home/moonui2/moon-erp-be/Modules/Core/app/Services/UnitConversionService.php` (WP0, committed a0d0992): `toBase(int $productId, ?int $unitId, float $qty): float` and `factorToBase(int $productId, ?int $unitId): float`. Identity (1.0) when unitId null / ==base / unresolved.

## What to implement
1. **Inject `UnitConversionService`** into StockService (constructor).
2. **Each qty-taking method accepts an optional `unit_id`** in its `$data` array (and needs `product_id`, which these methods already have). At the top of each method, resolve:
   - `$factor = $unitConversion->factorToBase($productId, $data['unit_id'] ?? null);`
   - `$baseQty = $rawQty * $factor;` — use `$baseQty` everywhere the method currently used the raw qty for stock/cost/FIFO/reserve math.
   - **Identity guarantee:** when `unit_id` is absent/null or `factor === 1.0`, `$baseQty === $rawQty` and NOTHING else changes — no new query beyond a cheap factor lookup that returns 1.0. Verify the method is byte-for-byte identical in that case.
   Methods to update: `increaseStock`, `decreaseStock`, `getIssueCost`, `reserve`, `release`. For `availableQuantity`: it RETURNS base on-hand (a query, not a movement) — do NOT convert its return; instead, if a caller needs "is this line-qty available", that comparison is converted at the CALLER in WP2 (or add an optional `unit_id` param that converts the COMPARISON input only if you find it cleaner — but keep the returned on-hand in base). Decide and document which you did.
3. **increaseStock — inverse unit_cost (CRITICAL [FIN]):** when converting qty to base, the unit_cost MUST be inverse-converted so total value is preserved: `base_unit_cost = unit_cost / factor` (so `base_qty * base_unit_cost == rawQty * unit_cost` = the line total). Example: 1 carton @ 12.000 (factor 12) → base_qty 12, base_unit_cost 1.000, total 12.000 (NOT 144.000). Write `base_unit_cost` + `base_qty` into the cost layer / movement / balance average-cost math. Confirm WAC and FIFO both stay correct.
4. **Migration (additive, nullable):** add `base_quantity DECIMAL(15,3) NULL` to the transaction line-item tables that feed stock: `sales_invoice_items`, `sales_delivery_note_items`, `sales_return_items`, `purchase_bill_items`, `purchase_grn_items`, `purchase_return_items`, `inventory_receipt_items`, `inventory_issue_items`, `inventory_transfer_items`, `inventory_adjustment_items` (+ production issue/material lines if they feed stock directly). Timestamp AFTER 2026_07_16. Nullable so existing rows are untouched (identity). **Run on dev same-step** (`bash local-deploy.sh` / `migrate --force`). ⛔ never migrate:fresh. NOTE: the STAMPING of base_quantity onto lines happens in WP2 (callers) — WP1 only adds the column. (If StockService is the natural place to compute base_qty, expose it so WP2 callers can persist it; do not persist from WP1 since no caller passes unit_id yet.)
5. **Reversal safety note (for WP2, record in a code comment):** cancel/return paths must reverse the SAME base qty the post moved. Since movements/layers are stored in base, reversing the stored MOVEMENT (not re-converting the line) is drift-proof. Verify the existing cancel path (CancelIssue / cancelled receipt) reverses stored base movements — if it re-reads line qty and would re-convert, flag it for WP2 (the stamped base_quantity is the fix).

## Acceptance criteria
- **Identity (SACRED):** every StockService call WITHOUT `unit_id` (i.e. all existing callers) behaves byte-for-byte as before → **Inventory feature suite 139/0 stays green with NO test changes**.
- **New capability tests:** calling `increaseStock`/`decreaseStock`/`getIssueCost` WITH a `unit_id` whose factor is 12: stock moves by base_qty (2 cartons → 24 base), inventory VALUE is preserved (2 carton @12 → total 24, base_unit_cost 1, NOT 288), a receipt→issue→return round-trip of a factor≠1 item nets on-hand and total value back to their starting values.
- Migration applied on dev; `base_quantity` column exists nullable on the listed tables.

## Tests
- New `Modules/Inventory/tests/Feature/StockServiceUnitConversionTest.php` (Pest/sqlite): identity (no unit_id == old behaviour), factor-12 increase (qty+value), factor-12 decrease + getIssueCost (cost on base), round-trip to zero. 
- Re-run the full Inventory feature suite → still 139/0 (no edits).

## Flags
- **[FIN]** — architect + Codex/code-reviewer review mandatory. **migration: yes** (additive/nullable, run on dev same-step).

## Out of scope
- Do NOT wire any caller to pass unit_id (that's WP2). Do NOT touch FE, pricing, availability comparison sites, or reservations wiring. Only StockService capability + the column.
