# WP4 — Pricing per unit + POS per-unit barcode [FIN]

## Goal
When a non-base unit is chosen on a line, the price should reflect that unit (a carton costs more than a kg). Per frozen decision D3: unit price = `product_units.sale_price`/`purchase_price` if set, else base price × conversion_factor. And POS must scan a product_units barcode (sell by carton via its own barcode) and know the product's units. Document totals stay in the ENTERED unit (qty × this unit price) — this is the revenue/price side; the stock/cost conversion is already done by WP1/WP2.

## Explore first
- **FE pricing:** where a line's `unit_price` is defaulted on product/unit pick (the shared grid `TransactionLineItemsComponent` + per-doc components; Phase-1: price comes from the product's `sale_price`, client-supplied, validated against base `min_sale_price`). Product model `product.model.ts` (`ProductUnit` has `sale_price`, `purchase_price`, `conversion_factor`). Use the WP3 `ProductUnitScopeService` product-units cache if helpful.
- **BE validation:** `Modules/Sales/app/Http/Requests/StoreSalesInvoiceRequest.php` (min_sale_price floor ~:44,94), POS `StorePOSSaleRequest.php`. The floor must scale with the unit (base min × factor) so a carton isn't rejected against the per-kg floor.
- **POS:** `Modules/POS/app/Http/Controllers/POSProductController.php:110-120` (barcode matches only products.barcode/sku) + `POSProductResource.php` (exposes base sale_price only). `POSSaleController.php` writes sales_invoice_items with unit_id.

## Implement
### FE — reprice on unit change (sales + purchases lines)
- When the user changes a line's unit (or picks a unit-scoped product), set the default `unit_price`:
  - sales: `product_units.sale_price` for that unit if set, else base `sale_price × conversion_factor`.
  - purchases: `product_units.purchase_price` if set, else base `purchase_price × conversion_factor`.
- Only DEFAULT the price (user can still override). Do this via a unit-change handler in the shared grid (the hook WP3 intentionally left for WP4). Keep qty in the entered unit.
### BE — price-floor validation per unit
- Sales min-price validation: the floor is base `min_sale_price × conversion_factor` for the chosen unit (so a carton line is validated against carton floor, not kg floor). Use `UnitConversionService::factorToBase(product, unit_id)`.
### POS — per-unit barcode + units
- `POSProductController::barcode`: also match `product_units.barcode`; when matched, return the product AND the matched `unit_id` (+ its price) so the POS line is created in that unit. Keep the existing products.barcode/sku match as fallback.
- `POSProductResource` / POS product lookup: include the product's `product_units` (unit, barcode, sale_price) so the POS UI can pick a unit.
- POS FE: use the returned unit_id + price when adding a scanned line.

## Acceptance criteria
- Sales/purchase line: picking the "carton" unit defaults the price to the carton price (product_units) or base×factor; overridable.
- A carton line priced above (carton floor) passes validation; the per-kg floor doesn't falsely reject it.
- POS: scanning a product_units barcode adds a line in that unit at that unit's price and (via WP1/WP2) deducts base stock correctly.
- Single-unit products: pricing unchanged (factor 1, no product_units → base price).
- `ng build` green; BE tests green vs baseline.

## Tests
- BE: a Pest test for the per-unit min-price floor (carton passes, absurdly-low fails) + POS barcode resolving product_units.barcode → correct unit_id + price. Reuse Sales/POS test patterns.
- FE: `ng build` green.

## Flags
- **[FIN]** (revenue/price defaults + validation floor) — code-reviewer review. No migration (product_units columns exist).

## Out of scope
- Stock/cost conversion (done WP1/WP2). Display of unit on reports (WP5). Seeding/validation hardening (WP5).
