# AmendOrderedPurchaseOrder

## Permission Scope

purchaseOrder

## Overview

AmendOrderedPurchaseOrder atomically amends an `ORDERED` purchase order. It supports both header field changes (`headerChanges`, an object patch keyed by field name) and line changes (`lineChanges`, an array of ADD / MODIFY / REMOVE operations). 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.

## 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: `headerChanges` is an object whose keys are header field names and whose values are the post-amendment value (or null); standard amendable fields (`paymentTermId`, `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
- HEADER: protected fields (`id`, `companyId`, `supplierId`, `orderStatus`, `receiptStatus`, `billingStatus`, `paymentTermSnapshotLines`, `supplierSnapshotName`, `rejectionReason`, `closeReason`, `createdAt`, `updatedAt`) are rejected at runtime as a defense-in-depth check (the standard type already excludes them)
- LINE: ADD and MODIFY accept app-extension fields via the generic `LCF` parameter alongside the standard typed fields; protected line fields (`id`, `purchaseOrderId`, `itemId`, `itemSnapshotName`, `itemSnapshotSku`, `unitId`, `createdAt`, `updatedAt`) are rejected at runtime when supplied via the extension escape hatch
- LINE: for MODIFY, 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
- HEADER: `paymentTermId` is amendable; when changed, it must reference an ACTIVE payment term and `paymentTermSnapshotLines` is refreshed from the selected term
- MODIFY: 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)
- MODIFY: quantity cannot be reduced below the line's cumulative non-cancelled received (posted + draft) or billed (matched + draft/blocked) quantity
- MODIFY: quantity must be positive, unitPrice must be non-negative
- MODIFY: receivingSiteId must exist if provided
- REMOVE: blocked if the line has any posted receipt or matched bill quantity
- REMOVE: blocked if draft inventory receipt evidence or draft/blocked purchase bills still reference the line
- 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 item (ACTIVE check skipped — post-approval flow)
- 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
- MODIFY of quantity, item, receiving site, order date, or physical-receipt requirement reconciles 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
- Toggling an active line away from physical receipt updates its existing supply plan to zero expected quantity; 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
- 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 MODIFY 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| D[Validate header changes against denylist]
    D -->|Fail| E[Return validation error]
    D -->|Pass| F[Validate each line change]
    F -->|Fail| E
    F -->|Pass| G[Apply header field updates to PurchaseOrder]
    G --> H[Process MODIFY lines: update in-place]
    H --> I[Process REMOVE lines: delete]
    I --> J[Process ADD lines: insert]
    J --> K[Reconcile inventory supply plans for affected lines]
    K --> L[Recalculate receiptStatus / billingStatus]
    L --> M[Write PurchaseOrderRevision envelope with MAX existing revisionNumber + 1]
    M --> N[Write PurchaseOrderFieldChange rows]
    N --> O[Return amended PO]
```

## External Dependencies

- [itemManagement::getItem](../../../item-management/docs/query/GetItem.md) - Validates item existence 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

## 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
- **PROTECTED_HEADER_FIELD**: A header change targets a protected field that cannot be amended
- **PROTECTED_LINE_FIELD**: A line ADD or MODIFY change targets a protected line field that cannot be amended
- **LINE_NOT_FOUND**: Referenced line does not exist on the target order
- **MODIFY_QUANTITY_BELOW_RECEIVED**: MODIFY reduces quantity below the line's non-cancelled received quantity (posted + draft)
- **MODIFY_QUANTITY_BELOW_BILLED**: MODIFY reduces quantity below the line's non-cancelled billed quantity (matched + draft/blocked)
- **REMOVE_LINE_HAS_RECEIPTS**: REMOVE targets a line with posted receipt quantity
- **REMOVE_LINE_HAS_BILLS**: REMOVE targets a line with matched bill quantity
- **REMOVE_LINE_HAS_DOWNSTREAM_REFERENCES**: REMOVE targets a line referenced by draft or blocked downstream documents
- **ADD_INVALID_QUANTITY**: ADD action has zero or negative quantity
- **ITEM_NOT_FOUND**: Referenced item does not exist
- **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 a MODIFY 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 MODIFY 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 MODIFY (null value)
- skips a MODIFIED row for a line MODIFY whose extension field value is unchanged
- supports combining header, MODIFY, 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
- amends paymentTermId and refreshes paymentTermSnapshotLines
- writes one PurchaseOrderRevision envelope per amendment
- writes PurchaseOrderFieldChange rows for line MODIFY (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)
- updates the unique inventory supply plan for a modified PO line
- zeros the existing supply plan when an active PO line no longer requires physical receipt

- returns error when both headerChanges and lineChanges are empty
- returns error when the amendment contains no actual field changes
- returns error when a header change targets a protected field
- returns error when a line MODIFY change targets a protected line field
- returns error when a line ADD change targets a protected line field
- returns error when paymentTermId header change references a missing payment term
- returns error when paymentTermId header change references an inactive payment term
- returns error when PO does not exist
- returns error when PO is not ORDERED
- returns error when MODIFY references a non-existent line
- returns error when MODIFY reduces quantity below received
- returns error when MODIFY reduces quantity below billed
- returns error when MODIFY reduces quantity below posted + draft received
- returns error when MODIFY reduces quantity below matched + draft/blocked billed
- returns error when REMOVE targets a line with posted receipts
- returns error when REMOVE targets a line with matched bills
- returns error when REMOVE targets a line referenced by draft downstream documents
- returns error when ADD has zero or negative quantity
- returns error when ADD references a non-existent item
- returns error when MODIFY has zero or negative quantity
- returns error when MODIFY has negative unitPrice
- returns error when MODIFY 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
- returns error when REMOVE targets a line referenced by draft or blocked bills
- writes ISO date strings in audit rows when header orderDate changes
- writes boolean strings in audit rows when line MODIFY toggles requiresPhysicalReceipt
- serializes numeric extension field values in line MODIFY audit rows
