# ApplyPurchaseChangeOrder

## Permission Scope

purchaseOrder

## Overview

ApplyPurchaseChangeOrder atomically applies an `APPROVED` change order to its target purchase order, superseding affected lines, creating new active lines with revised terms, incrementing the PO's revision number, and recalculating receipt and billing statuses using lineage carry-forward.

## Business Rules

- Target change order must exist
- Only change orders in `APPROVED` status can be applied
- Change order's `fromRevisionNumber` must match the purchase order's `currentRevisionNumber` (stale revision check)
- MODIFY action: supersedes the original ACTIVE line (sets `lineStatus` to `SUPERSEDED` and `supersededByLineId` to the new line), creates a new ACTIVE line inheriting `purchaseOrderId`, `itemId`, `unitId`, `receivingSiteId`, `requiresPhysicalReceipt`, and item snapshots with updated `quantity` and `unitPrice`, sets `rootLineId` to the original line's root (or the original line itself if it was a v1 line)
- MODIFY action: quantity cannot be below the lineage's cumulative received or billed quantity (re-validated at apply time)
- REMOVE action: supersedes the original ACTIVE line with no replacement (`supersededByLineId` is null)
- REMOVE action: blocked if the line has any posted receipts or matched bills (re-validated at apply time)
- ADD action: creates a new ACTIVE line with `rootLineId` set to null (it is its own lineage root)
- After processing all lines, the purchase order's `currentRevisionNumber` is incremented
- Receipt and billing statuses are recalculated using lineage carry-forward: effective received/billed quantity is the sum across all lines sharing the same `rootLineId`
- All changes are executed atomically

## Process Flow

```mermaid
flowchart TD
    A[Receive apply request] --> B{Change order exists?}
    B -->|No| C[Return CO_NOT_FOUND]
    B -->|Yes| D{Status is APPROVED?}
    D -->|No| E[Return CO_NOT_APPROVED]
    D -->|Yes| F{fromRevisionNumber matches PO currentRevisionNumber?}
    F -->|No| G[Return REVISION_MISMATCH]
    F -->|Yes| H[Re-validate MODIFY and REMOVE constraints]
    H -->|Fail| I[Return validation error]
    H -->|Pass| J[Process MODIFY lines: supersede original, create new ACTIVE line]
    J --> K[Process REMOVE lines: supersede original with no replacement]
    K --> L[Process ADD lines: create new ACTIVE line with null rootLineId]
    L --> M[Increment PO currentRevisionNumber]
    M --> N[Recalculate receiptStatus and billingStatus via lineage carry-forward]
    N --> O["Set change order status to APPLIED"]
    O --> P[Return applied change order]
```

## External Dependencies

- None

## Error Scenarios

- **CO_NOT_FOUND**: Target change order does not exist
- **CO_NOT_APPROVED**: Change order is not in `APPROVED` status
- **REVISION_MISMATCH**: Change order's `fromRevisionNumber` does not match the purchase order's `currentRevisionNumber`
- **MODIFY_QUANTITY_BELOW_RECEIVED**: MODIFY action reduces quantity below the lineage's cumulative received quantity
- **MODIFY_QUANTITY_BELOW_BILLED**: MODIFY action reduces quantity below the lineage's cumulative billed quantity
- **REMOVE_LINE_HAS_RECEIPTS**: REMOVE action targets a line with posted receipt quantity
- **REMOVE_LINE_HAS_BILLS**: REMOVE action targets a line with matched bill quantity

## Test Cases

- applies a valid approved change order with MODIFY lines
- applies a valid approved change order with REMOVE lines
- applies a valid approved change order with ADD lines
- increments PO currentRevisionNumber after application
- recalculates receiptStatus using lineage carry-forward after MODIFY from 100 to 120 with 100 received
- recalculates receiptStatus as RECEIVED after MODIFY from 100 to 80 with 80 received
- returns error when change order does not exist
- returns error when change order is not in APPROVED status
- returns PoNotFoundError when purchase order does not exist
- returns error when fromRevisionNumber does not match PO currentRevisionNumber
- returns error when MODIFY reduces quantity below received quantity at apply time
- returns error when MODIFY reduces quantity below billed quantity at apply time
- returns error when REMOVE targets a line with posted receipts at apply time
- returns error when REMOVE targets a line with matched bills at apply time
- defaults currentRevisionNumber to 1 when PO has null revision
- skips MODIFY when originalPurchaseOrderLineId is not found in PO lines
- uses original quantity/unitPrice when CO line has null overrides
- treats lines with null lineStatus as ACTIVE when filtering remaining lines
- returns the lineId itself when getLineageRoot receives an unknown lineId
- handles empty PO lines with empty GR/Bill line arrays
