# PostInventoryLedger

## Permission Scope

inventoryPosting

## Overview

postInventoryLedger is the single inventory-owned posting API. Document-owning modules (inbound-shipment, outbound-shipment) and inventory's own transfer/adjustment flows call it to record stock movements against the InventoryLedger in one transaction. Each call is one posting event identified by `(sourceType, sourceId)`; its lines can mix directions (for example a transfer ships one AVAILABLE OUT and one IN_TRANSIT IN as a single event).

The command creates immutable InventoryLedger rows carrying generic `sourceType`/`sourceId`/`sourceLineId` references, updates StockLevel balances, consumes matching open InventorySupplyPlan (for inbound lines with an order reference) or StockReservation (for available outbound lines), and applies valuation.

No intermediate posting document is created. The cause document (inbound shipment, outbound shipment, transfer order, stock adjustment) lives in its owning module or aggregate, and the ledger references it generically. Callers own document lifecycle (including the guarantee that a document posts exactly once) and unit conversion; this command owns reference validation, stock sufficiency, and the ledger/balance/valuation writes.

## Business Rules

- At least one movement line is required
- Each line quantity must be greater than zero and is expressed in the item's primary unit
- direction IN increases stock and direction OUT decreases it; the direction is recorded on the ledger row
- Each line carries an action — what the movement changes: QUANTITY_CHANGE (net on-hand), TRANSFER (location), or STOCK_TYPE_CHANGE (stock type). The action is direction-neutral; for QUANTITY_CHANGE the direction selects receipt vs issue costing, and (sourceType, action, direction) combinations with no defined treatment fail the posting
- Costing treatment: INBOUND_SHIPMENT × QUANTITY_CHANGE IN books the receipt against the accrual account; OUTBOUND_SHIPMENT × QUANTITY_CHANGE OUT relieves COGS; STOCK_ADJUSTMENT × QUANTITY_CHANGE posts an inventory gain (IN) or an inventory loss or scrap write-off (OUT) against the adjustment account; TRANSFER and STOCK_TYPE_CHANGE are internal relocations and produce no costing
- unitCost must be zero or greater when provided; on costed IN lines it is the actual acquisition cost, required for FIFO and AVERAGE items
- Costed lines are valued by the costing method of the policy assigned to the item in the movement's company; STANDARD items fail when they have no published standard
- Costed IN lines create a CostLayer and costed OUT lines consume layers, recording a CostLayerConsumption per consumed layer; a costed OUT that the company's layered stock cannot cover fails
- Costed lines create and post a balanced journal entry directly in financial-accounting in the same transaction (source INVENTORY_LEDGER). The movement's company is resolved from the location's site; line accounts come from the valuation policy assigned to the item in that company, and the accounting period from the company and effectiveDate
- Each line item and storage location must exist
- IN lines with an orderReference consume matching open InventorySupplyPlan quantity for the item and site before creating ledger rows; unmatched or missing references post without consumption
- A line's order reference (the demand/supply document it fulfills) is persisted on its ledger row regardless of direction, so acquisition cost adjustments can resolve a purchase receipt's cost layers by purchase order line
- After a purchase-order referenced IN line posts, any invoice price variance declared for the order line that its layers do not yet carry is redistributed in the same transaction — a zero-amount AcquisitionCostAdjustment anchored to the receipt ledger entry distributes the waiting or re-spread variance over the line's layers (see [PostAcquisitionCostAdjustment](PostAcquisitionCostAdjustment.md))
- OUT lines with stockType AVAILABLE always run the ATP check: reservation-aware site availability is enforced and matching open StockReservation quantity is consumed; postings that would draw down another demand's reservation are rejected
- OUT lines with a non-AVAILABLE stockType apply only a location-level non-negative check (no reservations exist for BLOCKED or IN_TRANSIT stock)
- Items without an ItemValuation record in the movement's company are auto-assigned that company's default valuation policy on their first costed movement; when the company has no default policy, the posting fails — costing is never silently skipped. TRANSFER and STOCK_TYPE_CHANGE lines resolve no policy and trigger no auto-assignment
- Every ledger row records action, sourceType, sourceId, the line's sourceLineId, and effectiveDate
- Ledger rows are immutable and append-only; returned entries preserve input line order

## Process Flow

```mermaid
flowchart TD
    A[Post inventory movements request] --> B{At least one line?}
    B -->|No| C[Return error: EMPTY_MOVEMENT_LINES]
    B -->|Yes| D[Validate item and location per line]
    D --> E{OUT line availability check}
    E -->|Insufficient| F[Return error: INSUFFICIENT_STOCK]
    E -->|OK| G[Consume reservations or supply plans]
    G --> H[Insert InventoryLedger rows]
    H --> I[Update StockLevel]
    I --> J[Resolve costing treatment from sourceType and action]
    J -->|No treatment defined| M2[Return error: COSTING_EVENT_NOT_SUPPORTED]
    J -->|TRANSFER or STOCK_TYPE_CHANGE| L[Return ledger entries in input order]
    J -->|Costed IN| P[Create cost layer]
    J -->|Costed OUT| S{Layered stock covers issue?}
    S -->|No| T[Return error: INSUFFICIENT_STOCK]
    S -->|Yes| U[Consume layers in FIFO order and record consumptions]
    P --> O{Costing method of assigned policy?}
    U --> O
    O -->|STANDARD| K{Published standard exists?}
    K -->|No| M[Return error: STANDARD_COST_NOT_FOUND]
    K -->|Yes| N[Create and post journal entry in financial-accounting]
    O -->|FIFO| V{IN without unitCost?}
    V -->|Yes| Q[Return error: UNIT_COST_REQUIRED]
    V -->|No| R[Value at layer cost]
    O -->|AVERAGE| W{IN without unitCost?}
    W -->|Yes| Q
    W -->|No| X[Value at the moving average]
    R --> N
    X --> N
    N --> L
```

## External Dependencies

- [item-management::Item](../../../item-management/docs/model/Item.md) - Validates referenced items
- [inventory::InventoryLedger](../model/InventoryLedger.md) - Records immutable stock ledger rows
- [inventory::StockLevel](../model/StockLevel.md) - Updates on-hand balances
- [inventory::InventorySupplyPlan](../model/InventorySupplyPlan.md) - Consumes matching open supply for inbound order-referenced lines
- [inventory::StockReservation](../model/StockReservation.md) - Consumes matching open reservations for available outbound lines
- [inventory::StandardCost](../model/StandardCost.md) - Resolves the current standard for STANDARD items
- [inventory::CostLayer](../model/CostLayer.md) - Created by every costed receipt and consumed by every costed issue
- [inventory::CostLayerConsumption](../model/CostLayerConsumption.md) - Records each layer slice a costed issue consumed
- [inventory::FifoCost](../model/FifoCost.md) - Layer price rows written by FIFO receipts and read by FIFO issues
- [inventory::AverageCost](../model/AverageCost.md) - Moving-average state and history appended by AVERAGE movements
- [inventory::ValuationPolicy](../model/ValuationPolicy.md) - Posting accounts and costing method of the policy assigned to the item in the movement's company
- [inventory::AcquisitionCostAdjustment](../model/AcquisitionCostAdjustment.md) - Receipt-triggered redistribution of declared invoice price variance
- [organization::Site](../../../organization/docs/model/Site.md) - Resolves the company whose ledger receives the costing journal
- [financial-accounting::JournalEntry](../../../financial-accounting/docs/model/JournalEntry.md) - Costing journals are created and posted for costed lines

## Error Scenarios

- **EMPTY_MOVEMENT_LINES**: No movement lines were provided
- **INVALID_QUANTITY**: Quantity is zero or negative
- **INVALID_UNIT_COST**: Unit cost is negative
- **ITEM_NOT_FOUND**: Referenced item does not exist
- **STORAGE_LOCATION_NOT_FOUND**: Referenced storage location does not exist
- **INSUFFICIENT_STOCK**: Available stock cannot cover the requested quantity
- **COSTING_EVENT_NOT_SUPPORTED**: No costing treatment is defined for the line's source type and action
- **SUPPLY_PLAN_NOT_OPEN**: Supply plan is not OPEN
- **INVALID_SUPPLY_PLAN_QUANTITIES**: Quantity values are invalid for a supply plan
- **STOCK_RESERVATION_NOT_OPEN**: Stock reservation is not OPEN
- **INVALID_RESERVATION_QUANTITIES**: Quantity values are invalid for a stock reservation
- **DEFAULT_VALUATION_POLICY_NOT_FOUND**: No valuation policy is assigned to the item and the movement company has no default policy
- **STANDARD_COST_NOT_FOUND**: The item has no published standard cost
- **UNIT_COST_REQUIRED**: An actual-cost (FIFO or AVERAGE) receipt line carries no unit cost
- **FIFO_COST_NOT_FOUND**: A cost layer has no price row
- **AVERAGE_COST_NOT_FOUND**: The item has no moving-average state in the company
- **SITE_NOT_FOUND**: Referenced site does not exist
- **ACCOUNTING_PERIOD_NOT_FOUND**: No accounting period covers the posting date
- **JOURNAL_ENTRY_CREATE_FAILED**: Journal entry creation failed
- **JOURNAL_ENTRY_POST_FAILED**: Journal entry posting failed

## Test Cases

- posts multi-line IN, consumes matching supply plans, and returns entries in input order
- consumes open reservations for ATP OUT of AVAILABLE stock
- posts mixed OUT/IN transfer legs without touching valuation
- stamps source references and effective date on ledger rows
- persists the order reference on order-referenced IN ledger rows
- redistributes variance declared before receipt onto the newly posted purchase receipt
- posts no redistribution when the order line has no declared variance
- returns error when no movement lines are provided
- returns error when quantity is not positive
- returns error when unit cost is negative
- returns error when item does not exist
- returns error when OUT exceeds location stock
- values STANDARD receipt at standard and creates a journal entry with purchase price variance lines
- records favorable purchase price variance when actual cost is below standard
- values STANDARD receipt at standard without variance when unit cost is not provided
- values STANDARD issue at standard and creates COGS journal lines
- posts adjustment gains and losses against the adjustment account
- writes scrap issues off against the adjustment account
- returns error when no costing treatment is defined for the source and action
- auto-assigns the movement company's default policy and posts costing for an untracked item
- returns error when no valuation policy is assigned and the movement company has no default
- returns error when a STANDARD item has no published standard cost
- returns error when a costed issue exceeds the layered stock
- values FIFO receipt at actual cost and creates a cost layer
- values FIFO issue at layer cost and records the consumed layer
- spans cost layers in FIFO order when an issue exceeds the oldest layer
- posts FIFO adjustment gains as a new layer and losses against the adjustment account
- returns error when a FIFO receipt has no unit cost
- values AVERAGE receipt at actual cost and folds it into the moving average
- values AVERAGE issue at the current moving average
- posts AVERAGE adjustment gains into the average and losses at the average
- starts the moving average from the first receipt
- returns error when an AVERAGE receipt has no unit cost
- returns error when no accounting period covers the movement date
- returns error when journal entry creation fails
