# PostInboundShipment

## Permission Scope

inboundShipmentOperation

## Overview

postInboundShipment posts a DRAFT InboundShipment. It locks the shipment, transitions the lifecycle from DRAFT to POSTED, and posts the resulting inbound stock effects through inventory's `postInventoryLedger` API in the same transaction. Inventory records the InventoryLedger rows, increases stock balances, and applies valuation; this module owns only the receiving document lifecycle.

The one-shot posting guarantee lives here: because posting is only permitted from DRAFT and the transition to POSTED happens before movements are handed to inventory, a shipment cannot be posted twice.

## Business Rules

- The target InboundShipment must exist and be in DRAFT status
- Posting is based on the InboundShipmentLine snapshot
- At least one shipment line must exist before posting
- The InboundShipment is locked FOR UPDATE and transitioned DRAFT to POSTED before movements are posted, guaranteeing one-shot posting
- All lines are posted through a single `inventory.commands.postInventoryLedger` call with `direction=IN`
- Each movement line's orderReference is built from the InboundShipmentLine's sourceDocumentType, sourceDocumentId, and sourceLineId; lines without source references post without an orderReference
- Ledger quantity uses primaryQuantity
- Before movements are posted, this shipment's received quantities are pushed as per-line deltas to purchase's `recalculatePurchaseOrderReceiptStatus`, which owns the received-quantity projection and receiptStatus rollup
- Unit cost is not taken from input: it is derived from the purchase-order line price read via purchase's `listPurchaseOrderLinesForMatching` after the receipt sync, so the shipment is valued at the price effective at posting time under purchase's lock — even when an invoice was posted first, the purchase-order price is used so account-payable acquisition-cost adjustments stay consistent
- The purchase-order line price is per source unit; the ledger unit cost is converted to the primary unit by dividing by unitConversionRate
- The derived unit cost is snapshotted onto each InboundShipmentLine's unitCost as the goods-receipt valuation audit trail
- postedAt is set when the shipment is marked POSTED after movements are posted
- Posting errors raised by inventory (for example insufficient stock) propagate to the caller and abort the transaction

## Process Flow

```mermaid
flowchart TD
    A[Post inbound shipment request] --> B[Lock InboundShipment FOR UPDATE]
    B --> C{DRAFT?}
    C -->|No| D[Return error: INVALID_STATUS]
    C -->|Yes| E[Load InboundShipmentLine rows]
    E --> F{At least one line?}
    F -->|No| G[Return error: EMPTY_SHIPMENT_LINES]
    F -->|Yes| H[Push received quantity deltas to purchase for PO-linked lines]
    H --> I[Read purchase-order line prices and derive primary-unit cost]
    I --> J[Build IN movement lines with orderReference from line source refs]
    J --> K[Call inventory.commands.postInventoryLedger once]
    K --> M[Set POSTED and postedAt, snapshot unitCost onto lines]
    M --> L[Return posted shipment id]
```

## External Dependencies

- [item-management::Item](../../../item-management/docs/model/Item.md) - Validates referenced items
- [inventory::InventoryLedger](../../../inventory/docs/model/InventoryLedger.md) - Records immutable stock ledger rows produced by posting
- [inventory::postInventoryLedger](../../../inventory/docs/command/PostInventoryLedger.md) - Posts the inbound stock movements, writing ledger, balances, and valuation
- [purchase::recalculatePurchaseOrderReceiptStatus](../../../purchase/docs/command/RecalculatePurchaseOrderReceiptStatus.md) - Recalculates PO receipt progress from inbound-shipment received-quantity deltas
- [purchase::listPurchaseOrderLinesForMatching](../../../purchase/docs/query/ListPurchaseOrderLinesForMatching.md) - Supplies the purchase-order line prices the ledger unit cost is derived from

## Error Scenarios

- **INVALID_STATUS**: Target entity is not in a valid status for this operation
- **EMPTY_SHIPMENT_LINES**: No inbound shipment lines were provided
- **ITEM_NOT_FOUND**: Referenced item does not exist
- **STORAGE_LOCATION_NOT_FOUND**: Referenced storage location does not exist
- **PURCHASE_ORDER_RECEIPT_SYNC_FAILED**: Purchase-order receipt status sync failed for the inbound shipment

## Test Cases

- returns error when inbound shipment is missing
- returns error when inbound shipment is not draft
- returns error when inbound shipment has no lines
- posts shipment lines into ledger and stock through the posting API
- keeps full precision when the unit-price conversion does not divide evenly
- snapshots the derived unit cost onto the shipment lines
- posts without a unit cost when no purchase-order line price is found
- builds the order reference from line source references when posting
- propagates inventory posting errors and leaves the shipment unposted
- marks the shipment POSTED with postedAt after movements are posted
- pushes aggregated received quantity deltas to purchase before posting movements
- returns error when purchase receipt sync fails
