# ConfirmStockAdjustment

## Permission Scope

inventoryApproval

## Overview

confirmStockAdjustment approves and executes a submitted stock adjustment, creating InventoryLedger entries and updating StockLevel records for each adjustment line. This combines approval and execution into a single step.

The ledger entries and stock level effects depend on the adjustment type (and for SCRAP, the `fromStockCategory` on each line):

- **CORRECTION INCREASE**: single DR entry, increases onHand
- **CORRECTION DECREASE**: single CR entry, decreases onHand
- **SCRAP (fromStockCategory=AVAILABLE, default)**: single CR entry, decreases onHand
- **SCRAP (fromStockCategory=BLOCKED)**: single CR entry against the BLOCKED type, removes blocked stock from inventory
- **BLOCK**: two entries (BLOCK/CR + BLOCK/DR), increases blocked (onHand unchanged)
- **UNBLOCK**: two entries (UNBLOCK/DR + UNBLOCK/CR), decreases blocked (onHand unchanged)

For serial-tracked items on SCRAP lines, the serial number transitions to SCRAPPED. The model permits this transition from either AVAILABLE (e.g., warehouse damage) or RETURNED (e.g., defective returned item) — both are supported.

## Business Rules

- Stock adjustment must exist
- Stock adjustment header is locked while confirming to prevent duplicate execution for the same adjustment
- Stock adjustment must be in SUBMITTED status
- Stock validation is performed from the DRAFT InventoryExecutionLine effects, aggregated by item, storage location, and stock type:
  - CORRECTION with DECREASE: onHand must be >= quantity
  - SCRAP with fromStockCategory=AVAILABLE (or unset): `onHand - blocked >= quantity` (cannot draw from blocked)
  - SCRAP with fromStockCategory=BLOCKED: `blocked >= quantity`
  - BLOCK: available stock `onHand - reserved - blocked >= quantity`
  - UNBLOCK: `blocked >= quantity`
- Transitions status from SUBMITTED to CONFIRMED
- Requires the DRAFT InventoryExecution created at submission time and transitions it to POSTED with postedAt set to the confirmation system timestamp
- For each line, creates ledger entries and updates stock levels as follows:
  - **CORRECTION INCREASE**: 1 ledger entry `{ CORRECTION, DR, quantity }` / StockLevel `onHand += qty` / Valuation DR (CostLayer create or recalc)
  - **CORRECTION DECREASE**: 1 ledger entry `{ CORRECTION, CR, quantity }` / StockLevel `onHand -= qty` / Valuation CR (layer consumption or recalc)
  - **SCRAP (fromStockCategory=AVAILABLE)**: 1 ledger entry `{ SCRAP, CR, quantity }` / StockLevel `onHand -= qty` / Valuation CR
  - **SCRAP (fromStockCategory=BLOCKED)**: 1 ledger entry `{ SCRAP, CR, quantity, stockType=BLOCKED }` / StockLevel `quantity -= qty` for the BLOCKED type / Valuation CR
  - **BLOCK**: 2 ledger entries `{ BLOCK, CR }` + `{ BLOCK, DR }` / StockLevel `blocked += qty` (onHand unchanged) / Valuation: no effect (internal transfer)
  - **UNBLOCK**: 2 ledger entries `{ UNBLOCK, DR }` + `{ UNBLOCK, CR }` / StockLevel `blocked -= qty` (onHand unchanged) / Valuation: no effect (internal transfer)
- Updates LotStockLevel when a line specifies lotId:
  - CORRECTION INCREASE: `onHand += qty`
  - CORRECTION DECREASE: `onHand -= qty`
  - SCRAP (AVAILABLE): `onHand -= qty`
  - SCRAP (BLOCKED): `onHand -= qty`, `blocked -= qty`
  - BLOCK: `blocked += qty`
  - UNBLOCK: `blocked -= qty`
- Updates SerialNumber state for serial-tracked lines:
  - SCRAP: SerialNumber transitions to SCRAPPED (permitted from AVAILABLE or RETURNED; any other state raises an error)
  - BLOCK/UNBLOCK: no SerialNumber state change (the block is at the StockLevel layer)
  - CORRECTION: typically not applicable to serial-tracked items; when present, no state change
- Ledger entries link to the StockAdjustment InventoryExecution through inventoryExecutionId
- When confirmed through module wiring, applies the DRAFT InventoryExecutionLine records and links InventoryLedger rows through inventoryExecutionId

## Process Flow

```mermaid
flowchart TD
    A[Confirm stock adjustment request] --> B{Adjustment exists?}
    B -->|No| C[Return error: STOCK_ADJUSTMENT_NOT_FOUND]
    B -->|Yes| D{Status is SUBMITTED?}
    D -->|No| E[Return error: INVALID_STATUS]
    D -->|Yes| F[Process each line]
    F --> G{Adjustment type?}
    G -->|CORRECTION INCREASE| H[Create Ledger DR\nonHand += qty\nValuation DR]
    G -->|CORRECTION DECREASE| J{onHand >= qty?}
    J -->|No| K[Return error: INSUFFICIENT_ON_HAND]
    J -->|Yes| L[Create Ledger CR\nonHand -= qty\nValuation CR]
    G -->|SCRAP + AVAILABLE| M{onHand - blocked >= qty?}
    M -->|No| N[Return error: INSUFFICIENT_ON_HAND]
    M -->|Yes| O[Create 1 Ledger CR\nonHand -= qty\nSerial AVAILABLE/RETURNED -> SCRAPPED\nValuation CR]
    G -->|SCRAP + BLOCKED| M2{blocked >= qty?}
    M2 -->|No| N2[Return error: INSUFFICIENT_BLOCKED_STOCK]
    M2 -->|Yes| O2[Create 1 Ledger entry\nSCRAP/CR from BLOCKED\nBLOCKED quantity -= qty\nSerial RETURNED -> SCRAPPED\nValuation CR]
    G -->|BLOCK| P{available >= qty?}
    P -->|No| Q[Return error: INSUFFICIENT_AVAILABLE_STOCK]
    P -->|Yes| R[Create 2 Ledger entries\nBLOCK/CR + BLOCK/DR\nblocked += qty]
    G -->|UNBLOCK| S{blocked >= qty?}
    S -->|No| T[Return error: INSUFFICIENT_BLOCKED_STOCK]
    S -->|Yes| U[Create 2 Ledger entries\nUNBLOCK/DR + UNBLOCK/CR\nblocked -= qty]
    H --> V[Update LotStockLevel if lotId]
    L --> V
    O --> V
    O2 --> V
    R --> V
    U --> V
    V --> W[Transition to CONFIRMED]
    W --> X[Return confirmed adjustment]
```

## External Dependencies

- [inventory::StockAdjustment](../model/StockAdjustment.md) - Validates existence and current status, updates status to CONFIRMED
- [inventory::StockAdjustmentLine](../model/StockAdjustmentLine.md) - Reads source line details such as cost, lot, and serial metadata
- [inventory::InventoryExecution](../model/InventoryExecution.md) - Reads the DRAFT stock adjustment execution, applies normalized stock effects, and marks it POSTED
- [inventory::InventoryLedger](../model/InventoryLedger.md) - Creates ledger entries per adjustment type and fromStockCategory
- [inventory::StockLevel](../model/StockLevel.md) - Validates stock sufficiency in the selected category and updates quantities
- [inventory::LotStockLevel](../model/LotStockLevel.md) - Updates lot-level onHand and blocked when lotId is present
- [inventory::SerialNumber](../model/SerialNumber.md) - Transitions serial number state (AVAILABLE/RETURNED → SCRAPPED for SCRAP)
- [inventory::CostLayer](../model/CostLayer.md) - Creates or consumes cost layers for CORRECTION/SCRAP types

## Error Scenarios

- **STOCK_ADJUSTMENT_NOT_FOUND**: Referenced stock adjustment does not exist
- **INVALID_STATUS**: Target entity is not in a valid status for this operation
- **INSUFFICIENT_ON_HAND**: On-hand quantity is insufficient for CORRECTION DECREASE, or available portion (onHand - blocked) is insufficient for SCRAP with fromStockCategory=AVAILABLE
- **INSUFFICIENT_AVAILABLE_STOCK**: Available stock is less than the requested quantity
- **INSUFFICIENT_BLOCKED_STOCK**: Blocked stock is less than the requested quantity
- **INVALID_SERIAL_STATE_FOR_SCRAP**: Serial number is not in AVAILABLE or RETURNED state and cannot be scrapped

## Test Cases

- returns error when stock adjustment does not exist
- locks the StockAdjustment row before validating status
- returns error when stock adjustment is not in SUBMITTED status
- returns error when stock adjustment is not in SUBMITTED status (CONFIRMED)
- returns error when on-hand is insufficient for CORRECTION DECREASE
- returns error when available portion is insufficient for SCRAP (AVAILABLE)
- returns error when blocked is insufficient for SCRAP (BLOCKED)
- returns error when available stock is insufficient for BLOCK
- returns error when blocked stock is insufficient for UNBLOCK
- confirms CORRECTION INCREASE and creates DR ledger entry (increases onHand)
- creates InventoryExecution linkage when confirming through module wiring
- creates new StockLevel when none exists for CORRECTION INCREASE
- confirms CORRECTION DECREASE and creates CR ledger entry (decreases onHand)
- confirms SCRAP (AVAILABLE) and creates 1 CR ledger entry (decreases onHand)
- confirms SCRAP (BLOCKED) and creates a single SCRAP/CR ledger entry
- confirms SCRAP (BLOCKED) and decreases the BLOCKED StockLevel type
- confirms SCRAP and transitions serial number from AVAILABLE to SCRAPPED
- confirms SCRAP and transitions serial number from RETURNED to SCRAPPED
- rejects SCRAP when serial is in ISSUED or SCRAPPED state
- confirms BLOCK and creates two ledger entries (CR + DR)
- confirms BLOCK and increases blocked without changing onHand
- confirms UNBLOCK and creates two ledger entries (DR + CR)
- confirms UNBLOCK and decreases blocked without changing onHand
- triggers valuation DR for CORRECTION INCREASE
- triggers valuation CR for CORRECTION DECREASE and SCRAP (both categories)
- does not trigger valuation for BLOCK and UNBLOCK
- updates LotStockLevel onHand when lotId provided on CORRECTION/SCRAP
- updates LotStockLevel blocked when lotId provided on BLOCK/UNBLOCK/SCRAP(BLOCKED)
- updates LotStockLevel blocked when lotId provided on UNBLOCK
- updates LotStockLevel onHand and blocked when lotId provided on SCRAP (BLOCKED)
- links ledger entries to the stock adjustment InventoryExecution
- rejects aggregate CORRECTION DECREASE when duplicate stock keys exceed on-hand
- applies aggregate CORRECTION INCREASE once per stock key
- updates lot stock from the InventoryExecutionLine snapshot
- validates scrap serial state from the InventoryExecutionLine snapshot
- transitions status from SUBMITTED to CONFIRMED
- processes multiple lines in a single confirmation
