# AmendOrderedPurchaseOrder

## Permission Scope

purchaseOrder

## Overview

AmendOrderedPurchaseOrder atomically amends an `ORDERED` purchase order. It supports both header field changes (`headerPatch`, an object patch keyed by field name) and line changes (`addLines`, `updateLines`, `removeLineIds`). For each affected field, a `PurchaseOrderFieldChange` row is recorded. All changes from a single call belong to one `PurchaseOrderRevision` envelope; the envelope's `revisionNumber` is `MAX(existing) + 1`, starting at 1. Receipt/billing statuses are recalculated. A unit-price change declares its acquisition-cost impact to inventory in the same transaction (see [PostAcquisitionCostAdjustment](../../../inventory/docs/command/PostAcquisitionCostAdjustment.md)).

## Business Rules

- Target purchase order must exist and be in `ORDERED` status
- At least one header or line field must actually change (empty or no-op amendments are rejected to prevent phantom revision increments)
- HEADER: `headerPatch` is an object whose keys are header field names and whose values are the post-amendment value (or null); standard amendable fields (`receivingSiteId`, `orderDate`, `externalSupplierOrderReference`) are typed; app-extension fields are accepted via the generic `CF` parameter
- HEADER: a key is interpreted as a change only when its value is not `undefined` (omitted keys leave the field untouched); `null` is allowed for nullable fields and signals an explicit clear
- LINE: ADD and UPDATE accept app-extension fields via the generic `LCF` parameter alongside the standard typed fields
- LINE: for UPDATE, an extension-field key is interpreted as a change only when its value is not `undefined`; `null` is allowed for nullable fields and signals an explicit clear
- HEADER: the live header column is updated with the typed value and the audit row stores its serialized string representation
- HEADER: `receivingSiteId` is amendable; when set, it must reference an existing site, and clearing it is rejected if any remaining physical-receipt line lacks its own receiving site
- UPDATE: updates the existing line in-place (same ID). Quantity and unitPrice are optional — omitting either preserves the current value (supports price-only amendments). unitId cannot be changed (UOM is immutable once ordered — use REMOVE + ADD instead)
- UPDATE: quantity cannot be reduced below the line's received or billed quantity projection
- UPDATE: quantity must be positive, unitPrice must be non-negative
- UPDATE: receivingSiteId must exist if provided
- REMOVE: blocked if the line has any received or billed quantity projection
- REMOVE: physically deletes the PurchaseOrderLine row; per-field snapshot rows preserve the pre-deletion state in PurchaseOrderFieldChange
- ADD: itemId and unitId are required; itemId must reference an existing, ACTIVE item that holds a [PurchaseItem](../model/PurchaseItem.md) record
- ADD: `requiresPhysicalReceipt` is resolved from the item's purchasing record and frozen onto the new line, exactly as at order creation
- UPDATE: `requiresPhysicalReceipt` cannot be amended — it belongs to the item's purchasing record; use REMOVE + ADD to re-resolve it
- ADD: quantity must be positive, unitPrice must be non-negative
- ADD: receivingSiteId must exist if provided
- ADD: item snapshot (name, SKU) is captured at amendment time
- UPDATE of quantity, receiving site, or order date propagates to the one linked inventory supply plan for that PO line
- ADD of a physical-receipt line creates an inventory supply plan using sourceType `PURCHASE_ORDER`, the PO id as `sourceId`, and the new PO line id as `sourceLineId`
- REMOVE of a PO line closes any open inventory supply plans linked to that line
- Terminal line removal, order cancellation, or order closure closes the plan
- Clearing or moving the header receiving site updates inventory supply plans for physical-receipt lines that inherit the header site
- UPDATE of unitPrice declares the price change to inventory's acquisition-cost register with variance kind ORDER_PRICE: one line per repriced order line with the signed amount `(new − old) × (receivedQuantity − billedQuantity)`, sourced to the revision envelope; a zero amount declares nothing
- After processing all changes, exactly one `PurchaseOrderRevision` envelope row is written with `revisionNumber = MAX(existing envelopes for this PO) + 1` (1 when none exist)
- For each modified header field: one `PurchaseOrderFieldChange` row with `recordType=HEADER`, `changeKind=MODIFIED`
- For each UPDATE line: one row per actually-changed field with `recordType=LINE`, `changeKind=MODIFIED`
- For each ADD line: one row per non-null snapshot field with `recordType=LINE`, `changeKind=ADDED`, `oldValue=null`
- For each REMOVE line: one row per non-null snapshot field with `recordType=LINE`, `changeKind=REMOVED`, `newValue=null`
- Receipt and billing statuses are recalculated from remaining PO lines

## Process Flow

```mermaid
flowchart TD
    A[Receive amend request] --> B{PO exists and ORDERED?}
    B -->|No| C[Return error]
    B -->|Yes| C2{Actual header or line changes?}
    C2 -->|No| C
    C2 -->|Yes| F[Validate each line change]
    F -->|Fail| E[Return validation error]
    F -->|Pass| G[Apply header field updates to PurchaseOrder]
    G --> H[Process UPDATE lines: update in-place]
    H --> I[Process REMOVE lines: delete]
    I --> J[Process ADD lines: insert]
    J --> K[Propagate changes to inventory supply plans]
    K --> L[Recalculate receiptStatus / billingStatus]
    L --> M[Write PurchaseOrderRevision envelope with MAX existing revisionNumber + 1]
    M --> N[Write PurchaseOrderFieldChange rows]
    N --> N2[Declare price changes' variance to inventory]
    N2 --> O[Return purchase order id]
```

## External Dependencies

- [itemManagement::getItem](../../../item-management/docs/query/GetItem.md) - Validates item existence and ACTIVE status for ADD actions
- [inventory::GetInventorySupplyPlan](../../../inventory/docs/query/GetInventorySupplyPlan.md) - Finds the unique supply plan linked to an amended PO line
- [inventory::CreateInventorySupplyPlan](../../../inventory/docs/command/CreateInventorySupplyPlan.md) - Creates supply plans for added physical-receipt lines
- [inventory::UpdateInventorySupplyPlan](../../../inventory/docs/command/UpdateInventorySupplyPlan.md) - Updates supply plans for amended physical-receipt lines
- [inventory::CloseInventorySupplyPlan](../../../inventory/docs/command/CloseInventorySupplyPlan.md) - Closes supply plans for removed lines using `SOURCE_LINES` input
- [inventory::PostAcquisitionCostAdjustment](../../../inventory/docs/command/PostAcquisitionCostAdjustment.md) - Records a price change's variance on the unbilled received quantity

## Error Scenarios

- **PO_NOT_FOUND**: Target purchase order does not exist
- **PO_NOT_ORDERED**: Purchase order is not in `ORDERED` status
- **EMPTY_AMENDMENT_CHANGES**: Amendment contains no header or line changes
- **LINE_NOT_FOUND**: Referenced line does not exist on the target order
- **MODIFY_QUANTITY_BELOW_RECEIVED**: UPDATE reduces quantity below the line's received quantity projection
- **MODIFY_QUANTITY_BELOW_BILLED**: UPDATE reduces quantity below the line's billed quantity
- **REMOVE_LINE_HAS_RECEIPTS**: REMOVE targets a line with posted receipt quantity
- **REMOVE_LINE_HAS_BILLS**: REMOVE targets a line with billed quantity
- **EMPTY_PURCHASE_ORDER_LINES**: Purchase order has no lines
- **ITEM_NOT_FOUND**: Referenced item does not exist
- **ITEM_NOT_ACTIVE**: Referenced item is not in ACTIVE status
- **ITEM_NOT_PURCHASABLE**: Referenced item holds no purchasing record and cannot be purchased
- **INVALID_QUANTITY**: Quantity is zero or negative
- **INVALID_UNIT_PRICE**: Unit price is negative or otherwise invalid
- **RECEIVING_SITE_NOT_FOUND**: Referenced receiving site does not exist
- **RECEIVING_SITE_REQUIRED**: A physical-receipt line has no receiving site

## Test Cases

- amends a PO with an UPDATE action (line ID unchanged, values updated)
- amends a PO with a REMOVE action (line physically deleted)
- amends a PO with an ADD action (new line created, no line receivingSiteId — header is used)
- amends a PO line with extension fields via UPDATE and records LINE MODIFIED rows for changed extension fields
- amends a PO line with extension fields via ADD and records LINE ADDED rows for non-null extension fields
- writes PurchaseOrderFieldChange rows for line REMOVE including non-null extension fields
- amends a PO line with explicit clear of an extension field via UPDATE (null value)
- skips a MODIFIED row for a line UPDATE whose extension field value is unchanged
- supports combining header, UPDATE, ADD, and REMOVE in a single amendment
- amends a PO header field (e.g. extension field) and records a HEADER MODIFIED row
- amends receivingSiteId as a validated header default
- writes one PurchaseOrderRevision envelope per amendment
- writes PurchaseOrderFieldChange rows for line UPDATE (only changed fields)
- writes PurchaseOrderFieldChange rows for line ADD (one per non-null snapshot field)
- writes PurchaseOrderFieldChange rows for line REMOVE (one per non-null snapshot field)
- increments envelope revisionNumber from the existing maximum
- recalculates receiptStatus after amendment
- recalculates billingStatus after amendment
- supports price-only amendment (quantity not provided)
- declares the price change's variance for the received but unbilled quantity
- skips the variance declaration when the repriced line is fully billed
- declares a negative variance for a line billed beyond receipts
- updates the unique inventory supply plan for a modified PO line
- leaves supply plans untouched when the amendment does not change expected receipts

- returns error when neither headerPatch nor line changes are provided
- returns error when the amendment contains no actual field changes
- returns error when PO does not exist
- returns error when PO is not ORDERED
- returns error when UPDATE references a non-existent line
- returns error when UPDATE reduces quantity below received
- returns error when UPDATE reduces quantity below billed
- returns error when REMOVE targets a line with posted receipts
- returns error when REMOVE targets a line with billed quantity
- returns error when ADD has zero or negative quantity
- returns error when amendment removes all remaining lines
- returns error when ADD references a non-existent item
- returns error when ADD references an inactive item
- returns error when ADD references an item with no purchasing record
- returns error when UPDATE has zero or negative quantity
- returns error when UPDATE has negative unitPrice
- returns error when UPDATE would leave a physical-receipt line without a receiving site
- returns error when ADD would create a physical-receipt line without a receiving site
- returns error when clearing header receivingSiteId leaves a remaining physical-receipt line without one
- writes ISO date strings in audit rows when header orderDate changes
- serializes numeric extension field values in line UPDATE audit rows
