# AmendConfirmedSalesOrder

## Permission Scope

salesOrder

## Overview

AmendConfirmedSalesOrder atomically amends a `CONFIRMED` sales 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 `SalesOrderFieldChange` row is recorded. All changes from a single call belong to one `SalesOrderRevision` envelope; the envelope's `revisionNumber` is `MAX(existing) + 1`, starting at 1. Fulfillment and billing statuses are recalculated from the remaining lines. Unlike the purchase side, a sales order has no inventory supply plan or acquisition-cost register, so a sales amendment touches only the order, its lines, and the revision audit trail.

## Business Rules

- Target sales order must exist and be in `CONFIRMED` 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 (`orderDate`, `shippingAddress`, `billingAddress`) 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: the live header column is updated with the typed value and the audit row stores its serialized string representation
- 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
- 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 confirmed — use REMOVE + ADD instead)
- UPDATE: quantity cannot be reduced below the line's fulfilled or billed quantity projection
- UPDATE: quantity must be positive, unitPrice must be non-negative
- REMOVE: blocked if the line has any fulfilled or billed quantity projection
- REMOVE: physically deletes the SalesOrderLine row; per-field snapshot rows preserve the pre-deletion state in SalesOrderFieldChange
- ADD: itemId is required; itemId must reference an ACTIVE item belonging to the order's company and holding a [SalesItem](../model/SalesItem.md) record (same policy as CreateSalesOrder)
- ADD: quantity must be positive, unitPrice must be non-negative
- ADD: `requiresPhysicalFulfillment` is resolved from the item's selling record and frozen onto the new line, exactly as at order creation
- UPDATE: `requiresPhysicalFulfillment` cannot be amended — it belongs to the item's selling record; use REMOVE + ADD to re-resolve it
- After processing all changes, exactly one `SalesOrderRevision` envelope row is written with `revisionNumber = MAX(existing envelopes for this SO) + 1` (1 when none exist)
- For each modified header field: one `SalesOrderFieldChange` 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`
- Fulfillment and billing statuses are recalculated from remaining SO lines

## Process Flow

```mermaid
flowchart TD
    A[Receive amend request] --> B{SO exists and CONFIRMED?}
    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 SalesOrder]
    G --> H[Process UPDATE lines: update in-place]
    H --> I[Process REMOVE lines: delete]
    I --> J[Process ADD lines: insert]
    J --> L[Recalculate fulfillmentStatus / billingStatus]
    L --> M[Write SalesOrderRevision envelope with MAX existing revisionNumber + 1]
    M --> N[Write SalesOrderFieldChange rows]
    N --> O[Return amended sales order and revision ids]
```

## External Dependencies

- [itemManagement::getItem](../../../item-management/docs/query/GetItem.md) - Validates the referenced item exists, is ACTIVE, and belongs to the order's company for ADD actions

## Error Scenarios

- **SALES_ORDER_NOT_FOUND**: Referenced sales order does not exist.
- **SALES_ORDER_NOT_CONFIRMED**: Sales order is not in `CONFIRMED` status
- **EMPTY_AMENDMENT_CHANGES**: Amendment contains no header or line changes
- **LINE_NOT_FOUND**: Referenced line does not exist on the target document
- **MODIFY_QUANTITY_BELOW_FULFILLED**: UPDATE reduces quantity below the line's fulfilled quantity projection
- **MODIFY_QUANTITY_BELOW_BILLED**: UPDATE reduces quantity below the line's billed quantity
- **REMOVE_LINE_HAS_FULFILLMENTS**: REMOVE targets a line with fulfilled quantity
- **REMOVE_LINE_HAS_BILLS**: REMOVE targets a line with billed quantity
- **EMPTY_SALES_ORDER_LINES**: Sales order has no lines
- **ITEM_NOT_FOUND**: One or more referenced items do not exist
- **ITEM_NOT_ACTIVE**: One or more referenced items are not in ACTIVE status
- **ITEM_NOT_SELLABLE**: Referenced item holds no selling record and cannot be sold
- **CROSS_COMPANY_REFERENCE**: One or more referenced records belong to a different company
- **INVALID_QUANTITY**: Quantity is zero or negative
- **INVALID_UNIT_PRICE**: Unit price must be greater than zero.

## Test Cases

- amends an SO with an UPDATE action (line ID unchanged, values updated)
- amends an SO with a REMOVE action (line physically deleted)
- amends an SO with an ADD action (new line created)
- amends an SO header field and records a HEADER MODIFIED row
- increments envelope revisionNumber from the existing maximum
- supports a price-only amendment (quantity not provided)
- returns error when the amendment contains no actual field changes
- returns error when SO is not CONFIRMED
- returns error when UPDATE reduces quantity below fulfilled
- returns error when ADD references a non-existent item
- returns error when ADD references an item with no selling record
