# PostOutboundShipment

## Permission Scope

outboundShipmentOperation

## Overview

postOutboundShipment posts a DRAFT OutboundShipment. It locks the shipment, transitions it DRAFT to POSTED, and issues stock by calling inventory's posting API once with the shipment's outbound lines. Inventory creates the InventoryLedger rows, decreases AVAILABLE StockLevel balances, applies valuation, and consumes matching reservations. The command returns the posted header; lines and ledger entries are available through queries.

## Business Rules

- The target OutboundShipment must exist and be in DRAFT status
- At least one shipment line must exist before posting
- Posting is one-shot: the DRAFT to POSTED transition is guaranteed here, so a shipment can be posted exactly once
- The OutboundShipment row is locked FOR UPDATE before posting to serialize concurrent post attempts
- Posting is based on the OutboundShipmentLine snapshot
- Inventory stock movement is applied by calling `inventory.commands.postInventoryLedger` exactly once with all lines as direction=OUT movements
- Ledger quantity uses primaryQuantity
- Each line's `orderReference` passed to the posting API is built from the line's source references (sourceDocumentType, sourceDocumentId, sourceLineId)
- Reservation consumption happens inside the posting API, not in this command; matching reservations are consumed there
- The shipment is marked POSTED with postedAt after the posting API returns successfully
- Sales-order shipment quantities are synchronized from SALES_ORDER source lines in the same transaction
- Stock availability is validated by the posting API; those errors pass through to this command

## Process Flow

```mermaid
flowchart TD
    A[Post outbound shipment request] --> B[Lock OutboundShipment FOR UPDATE]
    B --> C{DRAFT?}
    C -->|No| D[Return error: INVALID_STATUS]
    C -->|Yes| E[Load OutboundShipmentLine rows]
    E --> F{At least one line?}
    F -->|No| G[Return error: EMPTY_SHIPMENT_LINES]
    F -->|Yes| H[Transition DRAFT to POSTED]
    H --> I[Call inventory.commands.postInventoryLedger once, direction=OUT]
    I --> J[Set POSTED and postedAt]
    J --> K[Sync sales-order fulfillment projections]
    K --> L[Return posted shipment id]
```

## External Dependencies

- [item-management::Item](../../../item-management/docs/model/Item.md) - Validates referenced items during posting
- [outbound-shipment::OutboundShipment](../model/OutboundShipment.md) - Reads and posts the shipment header, applying outbound shipment lines
- [inventory::InventoryLedger](../../../inventory/docs/model/InventoryLedger.md) - Records the immutable stock ledger rows returned as inventoryLedgerEntries
- [inventory::StockLevel](../../../inventory/docs/model/StockLevel.md) - Decreases available stock through the posting API
- [sales::SalesOrder](../../../sales/docs/model/SalesOrder.md) - Receives shipped-quantity deltas and recalculates fulfillmentStatus

## Error Scenarios

- **INVALID_STATUS**: Target entity is not in a valid status for this operation
- **EMPTY_SHIPMENT_LINES**: No outbound shipment lines were provided
- **INSUFFICIENT_STOCK**: Available stock cannot cover the requested quantity
- **ITEM_NOT_FOUND**: Referenced item does not exist
- **STORAGE_LOCATION_NOT_FOUND**: Referenced storage location does not exist
- **SALES_ORDER_FULFILLMENT_SYNC_FAILED**: Sales-order fulfillment projection could not be synchronized

## Test Cases

- returns error when shipment is missing
- returns error when shipment is not in DRAFT status
- returns error when shipment has no lines
- synchronizes SALES_ORDER source-line quantities with sales fulfillment status
- aggregates shipped quantities per sales-order line before the sync
- builds the order reference from line source references
- surfaces INSUFFICIENT_STOCK from the inventory posting API
- returns error when the sales fulfillment sync fails
