Orders.ps1

# Pilot orders: read Pilot carrier orders, translate them, and stage each in TMW DataExchange at EDI state 10.

function New-PilotReferenceIndex {
  param([Parameter(Mandatory)][AllowEmptyCollection()]$Records, [Parameter(Mandatory)][string]$Key)
  $index = @{}
  foreach ($record in @($Records)) {
    $value = $record.$Key
    if ($null -eq $value -or [string]$value -eq '') { continue }
    $id = [string]$value
    if ($index.ContainsKey($id)) { throw "Pilot reference endpoint returned duplicate $Key $id." }
    $index[$id] = $record
  }
  $index
}

function Get-PilotReferenceData {
  param($Session)
  $locations = @(Get-PilotLocation -Session $Session)
  $terminals = @(Get-PilotTerminal -Session $Session)
  $contracts = @(Get-PilotContract -Session $Session)

  [pscustomobject]@{
    locations = New-PilotReferenceIndex @($locations) 'locationSystemId'
    terminals = New-PilotReferenceIndex @($terminals) 'terminalId'
    contracts = New-PilotReferenceIndex @($contracts) 'contractSystemId'
  }
}

function Assert-PilotAddress {
  param([Parameter(Mandatory)]$Address, [string]$Description)
  foreach ($field in @('address1', 'city', 'state', 'zip')) {
    if ([string]::IsNullOrWhiteSpace([string]$Address.$field)) {
      throw "$Description has no $field in Pilot reference data."
    }
  }
}

function ConvertFrom-PilotOrder {
  param(
    [Parameter(Mandatory)]$Record,
    $Cfg,
    [Parameter(Mandatory)]$References
  )

  if (-not $Record.dispatchOrderId) {
    throw 'Pilot order record has no dispatchOrderId.'
  }

  $items = @($Record.dispatchOrderItems | Where-Object { -not $_.isDeleted })
  if (-not $items.Count) {
    throw "Pilot order $($Record.dispatchOrderId) has no active dispatchOrderItems."
  }
  if (-not $Record.deliveryWindowStartDateTime -or -not $Record.deliveryWindowEndDateTime) {
    throw "Pilot order $($Record.dispatchOrderId) has no complete delivery window."
  }
  $deliveryWindowStart = [datetime]$Record.deliveryWindowStartDateTime
  $deliveryWindowEnd = [datetime]$Record.deliveryWindowEndDateTime
  if ($deliveryWindowEnd -le $deliveryWindowStart) {
    throw "Pilot order $($Record.dispatchOrderId) delivery window must end after it starts."
  }

  $stopsByKey = [ordered]@{}
  foreach ($item in $items) {
    $terminalId = $item.lineOfOperationsSystemId
    $locationId = if ($null -ne $item.locationSystemId) {
      $item.locationSystemId
    } else {
      $item.customerLocationSystemId
    }
    if ($null -eq $locationId) {
      throw "Pilot order $($Record.dispatchOrderId) item $($item.dispatchOrderItemId) has no delivery-location identifier."
    }
    if ($null -eq $item.dispatchOrderItemId) {
      throw "Pilot order $($Record.dispatchOrderId) has an item with no dispatchOrderItemId."
    }
    if ($null -eq $item.gallons -or [decimal]$item.gallons -le 0) {
      throw "Pilot order $($Record.dispatchOrderId) item $($item.dispatchOrderItemId) has no positive gallon quantity."
    }
    if ($null -eq $item.productSystemId -and $null -eq $item.wraProductSystemId) {
      throw "Pilot order $($Record.dispatchOrderId) item $($item.dispatchOrderItemId) has no product identifier."
    }

    # Pilot's reference data only fills in names and addresses; TMW maps each stop by its id.
    # A missing id stages as UNKNOWN, an id their reference data lacks keeps the id, and either
    # way the stop comes over with no address for ops to finish in EDI.
    $terminal = if ($null -ne $terminalId) { $References.terminals[[string]$terminalId] }
    $location = $References.locations[[string]$locationId]
    $contract = if ($null -ne $item.looSystemId) { $References.contracts[[string]$item.looSystemId] }
    $supplierName = if ($contract.contractName) {
      [string]$contract.contractName
    } else {
      'NO SUPPLIER'
    }
    $supplierDescription = if ($contract.contractDescription) {
      [string]$contract.contractDescription
    } else {
      $supplierName
    }
    $terminalAddress = if ($terminal) {
      [pscustomobject]@{
        address1 = $terminal.address
        address2 = $terminal.address2
        city = $terminal.city
        state = if ($terminal.stateSystemCode) { $terminal.stateSystemCode } else { $terminal.state }
        zip = $terminal.postalCode
      }
    } else {
      [pscustomobject]@{ address1 = $null; address2 = $null; city = $null; state = $null; zip = $null }
    }
    $locationAddress = if ($location) {
      [pscustomobject]@{
        address1 = $location.address1
        address2 = $location.address2
        city = $location.city
        state = $location.stateCode
        zip = $location.postalCode
      }
    } else {
      [pscustomobject]@{ address1 = $null; address2 = $null; city = $null; state = $null; zip = $null }
    }
    if ($terminal) { Assert-PilotAddress $terminalAddress "Pilot terminal $terminalId" }
    if ($location) { Assert-PilotAddress $locationAddress "Pilot location $locationId" }

    $commodityName = if ($item.productName) {
      $item.productName
    } elseif ($item.wraProductDescription) {
      $item.wraProductDescription
    } elseif ($item.wraProductLongName) {
      $item.wraProductLongName
    } else {
      'NO PRODUCT'
    }
    $productCode = if ($item.productCode) {
      $item.productCode
    } elseif ($item.wraProductId) {
      [string]$item.wraProductId
    } else {
      [string]$item.wraProductSystemId
    }
    $freight = [pscustomobject]@{
      freightNumber = $null
      uom = 'GAL'
      quantity = $item.gallons
      weight = 0
      commodityName = $commodityName
      commodityCode = $productCode
      productCode = $productCode
      pilotOrderItemId = [string]$item.dispatchOrderItemId
      pilotSourceItemId = [string]$item.sourceSystemOrderItemId
      supplierContractId = if ($null -ne $item.looSystemId) { [string]$item.looSystemId } else { 'UNKNOWN' }
      supplierName = $supplierName
      supplierDescription = $supplierDescription
      productSystemId = if ($null -ne $item.productSystemId) {
        $item.productSystemId
      } else {
        $item.wraProductSystemId
      }
    }

    foreach ($stopSpec in @(
      [pscustomobject]@{
        type = 'PU'
        locationCode = if ($null -ne $terminalId) { [string]$terminalId } else { 'UNKNOWN' }
        name = if ($terminal.name) { $terminal.name } else { 'NO TERMINAL' }
        address = $terminalAddress
      }
      [pscustomobject]@{
        type = 'DR'
        locationCode = [string]$locationId
        name = if ($location.description) { $location.description } else { 'NO LOCATION' }
        address = $locationAddress
      }
    )) {
      $key = '{0}|{1}|{2}|{3}' -f $stopSpec.type, $stopSpec.locationCode,
        $deliveryWindowStart, $deliveryWindowEnd
      if (-not $stopsByKey.Contains($key)) {
        $stopsByKey[$key] = [pscustomobject]@{
          stopNumber = $null
          type = $stopSpec.type
          locationCode = $stopSpec.locationCode
          name = $stopSpec.name
          address1 = $stopSpec.address.address1
          address2 = $stopSpec.address.address2
          city = $stopSpec.address.city
          state = $stopSpec.address.state
          zip = $stopSpec.address.zip
          earliest = $deliveryWindowStart
          latest = $deliveryWindowEnd
          freight = [System.Collections.Generic.List[object]]::new()
        }
      }
      $stopsByKey[$key].freight.Add($freight)
    }
  }

  $contractIds = @(
    $items.looSystemId |
      Where-Object { $null -ne $_ } |
      Sort-Object -Unique
  )
  $orderedStops = @($stopsByKey.Values | Where-Object type -eq 'PU') +
    @($stopsByKey.Values | Where-Object type -eq 'DR')

  [pscustomobject]@{
    pilotOrderId = [string]$Record.dispatchOrderId
    pilotGuid = $Record.dispatchOrderGuid
    pilotSourceOrderId = [string]$Record.sourceSystemOrderId
    pilotOrderItemIds = @($items.dispatchOrderItemId | ForEach-Object { [string]$_ })
    contractSystemIds = $contractIds
    deliveryWindowStart = $deliveryWindowStart
    deliveryWindowEnd = $deliveryWindowEnd
    billto = if ($Cfg.create.billto) { [string]$Cfg.create.billto } else { $null }
    bolNumber = $Record.bolNumber
    kiosked = [bool]$Record.kiosked -or [int]$Record.dispatchOrderStatusTypeId -eq 4
    stops = $orderedStops
    raw = $Record
  }
}

function Select-PilotOrdersToProcess {
  param([Parameter(Mandatory)][AllowEmptyCollection()]$Records, $Cfg)

  $field = $Cfg.poll.status_field
  $skip = @($Cfg.poll.skip_values)
  @($Records) | Where-Object {
    if (-not $_.dispatchOrderId) { return $false }
    if ($null -ne $_.kiosked -and [bool]$_.kiosked) { return $false }
    if ($field -and $skip.Count) { return ($skip -notcontains $_.$field) }
    $true
  }
}

function Receive-PilotOrders {
  param($Cfg, $Session, [int]$WindowDays = 0)

  if (-not $WindowDays) {
    $WindowDays = if ($Cfg.poll.window_days) { [int]$Cfg.poll.window_days } else { 30 }
  }
  if ($WindowDays -lt 2) {
    throw "A $WindowDays-day Pilot window can drop loads that cross the boundary. Use 2 or more."
  }

  $start = [datetime]::Today
  $records = Get-PilotOrder -Session $Session -StartDate $start -EndDate $start.AddDays($WindowDays)
  $references = Get-PilotReferenceData $Session

  foreach ($record in @(Select-PilotOrdersToProcess $records -Cfg $Cfg)) {
    try {
      ConvertFrom-PilotOrder $record -Cfg $Cfg -References $references
    } catch {
      Write-Warning "Pilot order $($record.dispatchOrderId) deferred: $($_.Exception.Message)"
    }
  }
}

# Create one Pilot order in TMW and place it in the DataExchange accept queue.
#
# One order is one transaction: acquire its idempotency lock, build through TMW's dx_* procedures,
# insert the matching DX archive, and commit only after the order is EDI/PILOT/state 10. No flat file,
# no alternate delivery method, and no custom object in the vendor database.

$script:EdiVersion = '39'
$script:OrderNumberToken = '__TMW_ORDER_NUMBER__'
$script:SlotWidth = @{
  '02' = @{ 1 = 2; 2 = 2; 3 = 14; 4 = 1; 5 = 20; 6 = 12; 7 = 12; 8 = 12; 9 = 2 }
  '03' = @{ 1 = 2; 2 = 2; 3 = 2; 4 = 12; 5 = 12; 6 = 12 }
  '04' = @{ 1 = 2; 2 = 2; 3 = 6; 4 = 10; 13 = 50; 14 = 8 }
  '05' = @{ 1 = 2; 2 = 2; 3 = 3; 4 = 80 }
  '06' = @{ 1 = 2; 2 = 2; 3 = 2; 4 = 35; 5 = 35; 6 = 35; 7 = 20; 8 = 2; 9 = 9 }
}

function Get-TmwValue {
  param($Cfg, [string]$Section, [string]$Name, $Default)
  $group = if ($null -ne $Cfg) {
    $property = $Cfg.PSObject.Properties[$Section]
    if ($property) { $property.Value }
  }
  if ($null -ne $group) {
    $property = $group.PSObject.Properties[$Name]
    if ($property -and $null -ne $property.Value -and [string]$property.Value -ne '') {
      return $property.Value
    }
  }
  $Default
}

function Format-DxText {
  param([string]$RecordType, [int]$Slot, $Value)
  $width = $script:SlotWidth[$RecordType][$Slot]
  if (-not $width) { throw "No DX width for record $RecordType slot $Slot." }
  if ([string]$Value -eq $script:OrderNumberToken) { return $script:OrderNumberToken }
  $text = if ($null -eq $Value) { '' } else { [string]$Value }
  if ($text -match '[^\x20-\x7E]') { throw "DX record $RecordType slot $Slot contains non-ASCII text." }
  if ($text.Length -gt $width) { $text = $text.Substring(0, $width) }
  $text.PadRight($width)
}

function Format-DxDate {
  param($Value)
  if (-not $Value) { return '' }
  ([datetime]$Value).ToString('yyyyMMddHHmm')
}

function Format-DxImpliedDecimal {
  param([decimal]$Value, [int]$Width = 10)
  if ($Value -lt 0) { throw 'DX quantity cannot be negative.' }
  $scaled = $Value * 100
  if ($scaled -ne [decimal]::Truncate($scaled)) { throw 'DX quantity supports at most two decimal places.' }
  $text = $scaled.ToString('0', [Globalization.CultureInfo]::InvariantCulture)
  if ($text.Length -gt $Width) { throw "DX quantity exceeds its $Width-digit field." }
  $text.PadLeft($Width, '0')
}

function Get-DxArchiveFingerprint {
  param([Parameter(Mandatory)][AllowEmptyCollection()]$Rows)

  $lines = foreach ($row in @($Rows | Sort-Object sequence)) {
    $recordType = ([string]$row.fields.dx_field001).Trim()
    $fields = foreach ($name in @($row.fields.Keys | Sort-Object)) {
      # Source time identifies the intake attempt, not the customer's business payload.
      if (-not ($recordType -eq '02' -and $name -eq 'dx_field006')) {
        '{0}={1}' -f $name, [string]$row.fields[$name]
      }
    }
    '{0}|{1}|{2}|{3}' -f $row.sequence, $row.stopIndex, $row.freightIndex, ($fields -join '|')
  }
  $sha = [Security.Cryptography.SHA256]::Create()
  try {
    $bytes = [Text.Encoding]::UTF8.GetBytes(($lines -join "`n"))
    ([BitConverter]::ToString($sha.ComputeHash($bytes))).Replace('-', '').ToLowerInvariant()
  } finally {
    $sha.Dispose()
  }
}

function New-DxDetailRow {
  param(
    [string]$RecordType,
    [hashtable]$Slots = @{},
    [int]$StopIndex = 0,
    [int]$FreightIndex = 0
  )
  $fields = [ordered]@{
    dx_field001 = Format-DxText $RecordType 1 $RecordType
    dx_field002 = Format-DxText $RecordType 2 $script:EdiVersion
  }
  foreach ($slot in $Slots.Keys) {
    $fields[('dx_field{0:D3}' -f [int]$slot)] = Format-DxText $RecordType ([int]$slot) $Slots[$slot]
  }
  [pscustomobject]@{
    sequence = 0
    stopIndex = $StopIndex
    freightIndex = $FreightIndex
    fields = $fields
  }
}

function Assert-TmwOrder {
  param([Parameter(Mandatory)]$PilotOrder, $Cfg)
  $partner = [string](Get-TmwValue $Cfg 'create' 'partner' 'PILOT')
  $billto = if ($PilotOrder.billto) { [string]$PilotOrder.billto } else {
    [string](Get-TmwValue $Cfg 'create' 'billto' '')
  }
  $id = [string]$PilotOrder.pilotOrderId
  $stops = @($PilotOrder.stops)
  if (-not $id -or $id.Length -gt 30) { throw 'Pilot order ID is required and must fit TMW varchar(30).' }
  if (-not $billto -or $billto.Length -gt 8) { throw 'TMW bill-to is required and must fit varchar(8).' }
  if (-not $partner -or $partner.Length -gt 20) { throw 'TMW trading partner is required and must fit varchar(20).' }
  if ($stops.Count -lt 2) { throw "Pilot order $id requires at least two stops." }
  foreach ($stop in $stops) {
    if ($stop.type -notin @('PU', 'DR')) { throw "Pilot order $id has unsupported stop type '$($stop.type)'." }
    if (-not $stop.locationCode -or ([string]$stop.locationCode).Length -gt 8) {
      throw "Pilot order $id stop location '$($stop.locationCode)' must fit TMW varchar(8)."
    }
    if (-not $stop.earliest -or -not $stop.latest) { throw "Pilot order $id has an incomplete stop window." }
    # a stop with no address at all is for ops to finish in EDI; a partial one is bad data
    foreach ($field in @('address1', 'city', 'state', 'zip')) {
      if (($stop.address1 -or $stop.city -or $stop.state -or $stop.zip) -and [string]::IsNullOrWhiteSpace([string]$stop.$field)) {
        throw "Pilot order $id stop '$($stop.locationCode)' has no $field."
      }
    }
    foreach ($field in @(
      @{ name = 'name'; width = 100 }
      @{ name = 'address1'; width = 40 }
      @{ name = 'address2'; width = 40 }
      @{ name = 'city'; width = 18 }
      @{ name = 'state'; width = 6 }
      @{ name = 'zip'; width = 10 }
    )) {
      if ([string]$stop.($field.name) -and ([string]$stop.($field.name)).Length -gt $field.width) {
        throw "Pilot order $id stop $($field.name) must fit TMW varchar($($field.width))."
      }
    }
    if (@($stop.freight).Count -eq 0) { throw "Pilot order $id stop '$($stop.locationCode)' has no freight." }
    foreach ($freight in @($stop.freight)) {
      if ($null -eq $freight.quantity -or [decimal]$freight.quantity -le 0) {
        throw "Pilot order $id has a non-positive freight quantity."
      }
      if (-not $freight.commodityName) { throw "Pilot order $id has freight without a commodity name." }
      if ([string]$freight.commodityName -and ([string]$freight.commodityName).Length -gt 60) {
        throw "Pilot order $id commodity description must fit TMW varchar(60)."
      }
      if ([string]$freight.pilotOrderItemId -and ([string]$freight.pilotOrderItemId).Length -gt 30) {
        throw "Pilot order $id item ID must fit TMW varchar(30)."
      }
      if (-not $freight.supplierContractId) { throw "Pilot order $id has freight without a supplier contract ID." }
      if (-not $freight.supplierName) { throw "Pilot order $id has freight without a supplier name." }
    }
  }
}

function New-DxArchivePlan {
  param([Parameter(Mandatory)]$PilotOrder, $Cfg, [datetime]$Now = (Get-Date))
  Assert-TmwOrder $PilotOrder $Cfg
  $partner = [string](Get-TmwValue $Cfg 'create' 'partner' 'PILOT')
  $division = [string](Get-TmwValue $Cfg 'create' 'division' '')
  $scac = [string](Get-TmwValue $Cfg 'create' 'scac' '')
  $billto = if ($PilotOrder.billto) { [string]$PilotOrder.billto } else {
    [string](Get-TmwValue $Cfg 'create' 'billto' '')
  }
  $id = [string]$PilotOrder.pilotOrderId
  if (-not $division -or $division.Length -gt 6) { throw 'TMW division is required and must fit varchar(6).' }
  if (-not $scac -or $scac.Length -gt 8) { throw 'TMW SCAC is required and must fit varchar(8).' }
  $stops = @($PilotOrder.stops)
  $firstDate = ($stops | ForEach-Object { [datetime]$_.earliest } | Measure-Object -Minimum).Minimum
  $lastDate = ($stops | ForEach-Object { [datetime]$_.latest } | Measure-Object -Maximum).Maximum
  $rows = [System.Collections.Generic.List[object]]::new()

  $rows.Add((New-DxDetailRow '02' @{
    3 = $partner; 4 = 'N'; 5 = $script:OrderNumberToken
    6 = (Format-DxDate $Now); 7 = (Format-DxDate $firstDate); 8 = (Format-DxDate $lastDate); 9 = 'PP'
  }))
  foreach ($ref in @(
    @{ type = 'EDI'; value = 'YES' }
    @{ type = 'SCA'; value = $scac }
    @{ type = '_R1'; value = $division }
    @{ type = 'PO'; value = $id }
    @{ type = 'PO'; value = $script:OrderNumberToken }
    @{ type = 'SO'; value = $PilotOrder.pilotSourceOrderId }
  )) {
    if ($ref.value) { $rows.Add((New-DxDetailRow '05' @{ 3 = $ref.type; 4 = $ref.value })) }
  }
  $supplierNames = @(
    $stops |
      Where-Object type -eq 'PU' |
      ForEach-Object { $_.freight } |
      ForEach-Object { $_.supplierName } |
      Sort-Object -Unique
  )
  foreach ($supplierName in $supplierNames) {
    $rows.Add((New-DxDetailRow '05' @{ 3 = 'SUP'; 4 = $supplierName }))
    $rows.Add((New-DxDetailRow '06' @{ 3 = 'SU'; 4 = $supplierName }))
  }
  $rows.Add((New-DxDetailRow '06' @{ 3 = 'BT'; 4 = $billto }))

  for ($stopIndex = 1; $stopIndex -le $stops.Count; $stopIndex++) {
    $stop = $stops[$stopIndex - 1]
    $rows.Add((New-DxDetailRow '03' @{
      3 = $stop.type
      4 = (Format-DxDate $stop.earliest)
      5 = (Format-DxDate $stop.earliest)
      6 = (Format-DxDate $stop.latest)
    } $stopIndex))
    foreach ($ref in @(
      @{ type = 'ST#'; value = $stop.locationCode }
      @{ type = 'LOC'; value = $stop.locationCode }
      @{ type = 'PO'; value = $id }
    )) {
      $rows.Add((New-DxDetailRow '05' @{ 3 = $ref.type; 4 = $ref.value } $stopIndex))
    }
    $freight = @($stop.freight)
    for ($freightIndex = 1; $freightIndex -le $freight.Count; $freightIndex++) {
      $item = $freight[$freightIndex - 1]
      $rows.Add((New-DxDetailRow '04' @{
        3 = 'GAL'
        4 = (Format-DxImpliedDecimal ([decimal]$item.quantity))
        13 = $item.commodityName
        14 = ''
      } $stopIndex $freightIndex))
      if ($item.productCode) {
        $rows.Add((New-DxDetailRow '05' @{ 3 = 'L06'; 4 = $item.productCode } $stopIndex $freightIndex))
      }
      if ($item.pilotOrderItemId) {
        $rows.Add((New-DxDetailRow '05' @{ 3 = 'OID'; 4 = $item.pilotOrderItemId } $stopIndex $freightIndex))
        $rows.Add((New-DxDetailRow '05' @{ 3 = 'PO'; 4 = $item.pilotOrderItemId } $stopIndex $freightIndex))
      }
      if ($stop.type -eq 'PU') {
        $rows.Add((New-DxDetailRow '05' @{ 3 = 'SID'; 4 = $item.supplierContractId } $stopIndex $freightIndex))
        $rows.Add((New-DxDetailRow '05' @{ 3 = 'SUN'; 4 = $item.supplierName } $stopIndex $freightIndex))
      }
    }
    $rows.Add((New-DxDetailRow '06' @{
      3 = 'ST'; 4 = $stop.name; 5 = $stop.address1; 6 = $stop.address2
      7 = $stop.city; 8 = $stop.state; 9 = $stop.zip
    } $stopIndex))
  }

  $sequence = 0
  foreach ($row in $rows) { $sequence++; $row.sequence = $sequence }
  $sourcePrefix = "PILOTAPI.$id"
  $fingerprint = Get-DxArchiveFingerprint $rows.ToArray()
  [pscustomobject]@{
    sourceName = "$sourcePrefix.$fingerprint"
    sourcePrefix = $sourcePrefix
    fingerprint = $fingerprint
    sourceDate = $Now
    partner = $partner
    billto = $billto
    division = $division
    scac = $scac
    rows = $rows.ToArray()
  }
}

function New-TmwParameter {
  param([string]$Name, [string]$Type, [int]$Size, $Value)
  [pscustomobject]@{ name = $Name; type = $Type; size = $Size; value = $Value }
}

function New-TmwOrderCommand {
  param([Parameter(Mandatory)]$PilotOrder, $Cfg, [switch]$Commit, [datetime]$Now = (Get-Date))
  Assert-TmwOrder $PilotOrder $Cfg
  $plan = New-DxArchivePlan $PilotOrder $Cfg $Now
  $id = [string]$PilotOrder.pilotOrderId
  $deliveryStart = [datetime]$PilotOrder.deliveryWindowStart
  $deliveryEnd = [datetime]$PilotOrder.deliveryWindowEnd
  $remark = 'PO: {0} WINDOW: {1:HHmm}-{2:HHmm}' -f $id, $deliveryStart, $deliveryEnd
  if ($remark.Length -gt 254) { throw "Pilot order $id remark must fit TMW varchar(254)." }
  $expectedDatabase = [string](Get-TmwValue $Cfg 'tmw' 'expected_database' '')
  if (-not $expectedDatabase) { throw 'tmw.expected_database is required.' }

  $parameters = [System.Collections.Generic.List[object]]::new()
  foreach ($p in @(
    (New-TmwParameter '@expected_database' 'VarChar' 128 $expectedDatabase)
    (New-TmwParameter '@pilot_id' 'VarChar' 30 $id)
    (New-TmwParameter '@partner' 'VarChar' 20 $plan.partner)
    (New-TmwParameter '@billto' 'VarChar' 8 $plan.billto)
    (New-TmwParameter '@division' 'VarChar' 6 $plan.division)
    (New-TmwParameter '@scac' 'VarChar' 8 $plan.scac)
    (New-TmwParameter '@supplier' 'VarChar' 8 'UNKNOWN')
    (New-TmwParameter '@source_prefix' 'VarChar' 80 $plan.sourcePrefix)
    (New-TmwParameter '@source_name' 'VarChar' 255 $plan.sourceName)
    (New-TmwParameter '@fingerprint' 'VarChar' 64 $plan.fingerprint)
    (New-TmwParameter '@source_date' 'DateTime' 0 $plan.sourceDate)
    (New-TmwParameter '@commit' 'Bit' 0 ([bool]$Commit))
    (New-TmwParameter '@remark' 'VarChar' 254 $remark)
  )) { [void]$parameters.Add($p) }

  $sql = [System.Collections.Generic.List[string]]::new()
  $sql.AddRange([string[]]@(
    'set transaction isolation level read committed;'
    'set deadlock_priority -5;'
    'set nocount on;'
    'set xact_abort on;'
    "if db_name() <> @expected_database throw 50001, 'Pilot order target database mismatch', 1;"
    "if (select count(1) from dbo.edi_tender_partner where etp_partnerId = @partner) <> 1 throw 50002, 'PILOT tender partner is not exact', 1;"
    "if (select count(1) from dbo.edi_trading_partner where trp_id = @partner and cmp_id = @billto) <> 1 throw 50003, 'PILOT trading partner is not exact', 1;"
    "if (select count(1) from dbo.dx_xref where dx_importid = 'dx_204' and dx_trpid = @partner and dx_entitytype = 'TPSettings') <> 39 throw 50004, 'PILOT TPSettings are not exact', 1;"
    "if (select count(1) from dbo.company where cmp_id = 'UNKNOWN') <> 1 throw 50007, 'TMW UNKNOWN company is not exact', 1;"
    "if (select count(1) from dbo.commodity where cmd_code = 'UNKNOWN') <> 1 throw 50008, 'TMW UNKNOWN commodity is not exact', 1;"
    'begin try'
    ' begin transaction;'
    " declare @lock_result int, @lock_resource nvarchar(255) = 'pilot-order:' + @pilot_id; exec @lock_result = sys.sp_getapplock @Resource = @lock_resource, @LockMode = 'Exclusive', @LockOwner = 'Transaction', @LockTimeout = 15000;"
    " if @lock_result < 0 throw 50005, 'could not acquire Pilot order lock', 1;"
    ' declare @existing_hdr int = null, @existing_archive bigint = null, @existing_source_name varchar(255) = null;'
    " declare @existing_order_count int = (select count(distinct o.ord_hdrnumber) from dbo.orderheader o where o.ord_editradingpartner = @partner and (exists (select 1 from dbo.referencenumber r where r.ref_tablekey = o.ord_hdrnumber and r.ref_table = 'orderheader' and r.ref_type = 'PO' and r.ref_number = @pilot_id) or (o.ord_reftype = 'PO' and o.ord_refnum = @pilot_id)));"
    " if @existing_order_count > 1 throw 50016, 'duplicate TMW orders exist for Pilot order ID', 1;"
    " select top (1) @existing_hdr = o.ord_hdrnumber from dbo.orderheader o where o.ord_editradingpartner = @partner and (exists (select 1 from dbo.referencenumber r where r.ref_tablekey = o.ord_hdrnumber and r.ref_table = 'orderheader' and r.ref_type = 'PO' and r.ref_number = @pilot_id) or (o.ord_reftype = 'PO' and o.ord_refnum = @pilot_id));"
    " declare @existing_archive_count int = (select count(1) from dbo.dx_Archive_header where dx_importid = 'dx_204' and dx_trpid = @partner and (dx_sourcename = @source_prefix or left(dx_sourcename, len(@source_prefix) + 1) = @source_prefix + '.'));"
    " select top (1) @existing_archive = dx_Archive_header_id, @existing_source_name = dx_sourcename from dbo.dx_Archive_header where dx_importid = 'dx_204' and dx_trpid = @partner and (dx_sourcename = @source_prefix or left(dx_sourcename, len(@source_prefix) + 1) = @source_prefix + '.') order by dx_Archive_header_id desc;"
    " if (@existing_hdr is null and @existing_archive_count > 0) or (@existing_hdr is not null and @existing_archive_count = 0) throw 50006, 'partial Pilot order state exists; refusing automatic repair', 1;"
    ' if @existing_hdr is not null'
    ' begin'
    ' declare @existing_number varchar(12), @existing_state tinyint, @existing_status varchar(6); select @existing_number = rtrim(ord_number), @existing_state = ord_edistate, @existing_status = rtrim(ord_status) from dbo.orderheader where ord_hdrnumber = @existing_hdr;'
    " declare @existing_fingerprint varchar(64) = case when len(@existing_source_name) = len(@source_prefix) + 65 and left(@existing_source_name, len(@source_prefix) + 1) = @source_prefix + '.' then right(@existing_source_name, 64) end;"
    ' declare @replay_action varchar(40), @requires_review bit = 0;'
    " if @existing_archive_count > 1 select @replay_action = 'REVIEW_MULTIPLE_ARCHIVES', @requires_review = 1;"
    " else if @existing_source_name = @source_name select @replay_action = 'SKIPPED_UNCHANGED';"
    " else if @existing_fingerprint is null select @replay_action = 'REVIEW_CHANGED_UNFINGERPRINTED', @requires_review = 1;"
    " else if @existing_state = 10 select @replay_action = 'REVIEW_CHANGED_PENDING', @requires_review = 1;"
    " else if @existing_state in (40, 41, 42, 43, 45) select @replay_action = 'REVIEW_CHANGED_UPDATE_PENDING', @requires_review = 1;"
    " else select @replay_action = 'REVIEW_CHANGED_LOCKED', @requires_review = 1;"
    ' commit transaction;'
    " select cast(1 as bit) ok, @replay_action action, @requires_review requires_review, @pilot_id pilot_order_id, @existing_hdr order_hdrnumber, @existing_number order_number, @existing_archive archive_header_id, @existing_state edistate, @existing_status order_status, 0 detail_rows, @fingerprint incoming_fingerprint, @existing_fingerprint existing_fingerprint, db_name() database_name;"
    ' return;'
    ' end;'
    " declare @mv int = 0, @ret int = 1, @ordnum varchar(12) = '', @stop_number int = 0, @freight_number int = 0;"
    " declare @validate char(1) = 'N', @carrier varchar(8) = '', @dispatch_status varchar(6) = '', @leg_type varchar(6) = '';"
    ' declare @scope table (stop_index int not null, freight_index int not null, stop_number int not null, freight_number int not null, primary key (stop_index, freight_index));'
  ))

  $stops = @($PilotOrder.stops)
  for ($stopIndex = 1; $stopIndex -le $stops.Count; $stopIndex++) {
    $stop = $stops[$stopIndex - 1]
    $freight = @($stop.freight)
    $event = if ($stop.type -eq 'PU') { 'LLD' } else { 'LUL' }
    $timeWindow = if ($stop.type -eq 'DR') { 'Custom' } else { '' }
    foreach ($p in @(
      (New-TmwParameter "@cmp$stopIndex" 'VarChar' 8 ([string]$stop.locationCode))
      (New-TmwParameter "@name$stopIndex" 'VarChar' 100 ([string]$stop.name))
      (New-TmwParameter "@address1_$stopIndex" 'VarChar' 40 ([string]$stop.address1))
      (New-TmwParameter "@address2_$stopIndex" 'VarChar' 40 ([string]$stop.address2))
      (New-TmwParameter "@city_name$stopIndex" 'VarChar' 18 ([string]$stop.city))
      (New-TmwParameter "@state$stopIndex" 'VarChar' 6 ([string]$stop.state))
      (New-TmwParameter "@zip$stopIndex" 'VarChar' 10 ([string]$stop.zip))
      (New-TmwParameter "@arrival$stopIndex" 'DateTime' 0 ([datetime]$stop.earliest))
      (New-TmwParameter "@early$stopIndex" 'DateTime' 0 ([datetime]$stop.earliest))
      (New-TmwParameter "@late$stopIndex" 'DateTime' 0 ([datetime]$stop.latest))
      (New-TmwParameter "@window$stopIndex" 'VarChar' 15 $timeWindow)
      (New-TmwParameter "@cmd${stopIndex}_1" 'VarChar' 60 ([string]$freight[0].commodityName))
      (New-TmwParameter "@qty${stopIndex}_1" 'Float' 0 ([double]$freight[0].quantity))
      (New-TmwParameter "@oid${stopIndex}_1" 'VarChar' 30 ([string]$freight[0].pilotOrderItemId))
      (New-TmwParameter "@supid${stopIndex}_1" 'VarChar' 30 ([string]$freight[0].supplierContractId))
      (New-TmwParameter "@supname${stopIndex}_1" 'VarChar' 30 ([string]$freight[0].supplierName).Substring(0, [Math]::Min(30, ([string]$freight[0].supplierName).Length)))
    )) { [void]$parameters.Add($p) }
    # Raw Pilot IDs remain available for mapping while the pending stop carries readable address data.
    $sql.Add(" exec @ret = dbo.dx_add_neworder_stop 'N', @mv, $stopIndex, '$event', 'UNKNOWN', 0, 0, '', '', @arrival$stopIndex, @early$stopIndex, @late$stopIndex, 'UNKNOWN', @cmd${stopIndex}_1, 0, 'LBS', 0, 'PCS', @qty${stopIndex}_1, 'GAL', 'ST#', @cmp$stopIndex, 'OID', @oid${stopIndex}_1, '', 0, @mv output, @stop_number output, @freight_number output;")
    $sql.Add(" if @ret < 1 throw 50010, 'dx_add_neworder_stop failed', 1;")
    $sql.Add(" insert @scope values ($stopIndex, 1, @stop_number, @freight_number);")
    for ($freightIndex = 2; $freightIndex -le $freight.Count; $freightIndex++) {
      $item = $freight[$freightIndex - 1]
      foreach ($p in @(
        (New-TmwParameter "@cmd${stopIndex}_$freightIndex" 'VarChar' 60 ([string]$item.commodityName))
        (New-TmwParameter "@qty${stopIndex}_$freightIndex" 'Float' 0 ([double]$item.quantity))
        (New-TmwParameter "@oid${stopIndex}_$freightIndex" 'VarChar' 30 ([string]$item.pilotOrderItemId))
        (New-TmwParameter "@supid${stopIndex}_$freightIndex" 'VarChar' 30 ([string]$item.supplierContractId))
        (New-TmwParameter "@supname${stopIndex}_$freightIndex" 'VarChar' 30 ([string]$item.supplierName).Substring(0, [Math]::Min(30, ([string]$item.supplierName).Length)))
      )) { [void]$parameters.Add($p) }
      $sql.Add(" exec @ret = dbo.dx_add_neworder_freight_to_stop 'N', @stop_number, 'UNKNOWN', @cmd${stopIndex}_$freightIndex, 0, 'LBS', 0, 'PCS', @qty${stopIndex}_$freightIndex, 'GAL', 'OID', @oid${stopIndex}_$freightIndex, 0, '', 0, 0, '', 0, '', 0, '', 0, '', 0, '', @freight_number output;")
      $sql.Add(" if @ret < 1 throw 50011, 'dx_add_neworder_freight_to_stop failed', 1;")
      $sql.Add(" insert @scope values ($stopIndex, $freightIndex, @stop_number, @freight_number);")
    }
  }

  $sql.AddRange([string[]]@(
    " exec @ret = dbo.dx_create_order_from_stops @validate output, @mv, '', @billto, @source_date, 'DX', @billto, 'Y', @division, '', '', '', 0, 'PO', @pilot_id, @remark, 'T', 0, '', 0, 0, '', '', 0, 0, 'PP', '', 'N', @carrier output, @dispatch_status output, @leg_type output, 'N', 10, @partner, @supplier, @ordnum output;"
    " if @ret < 1 or nullif(rtrim(@ordnum), '') is null throw 50012, 'dx_create_order_from_stops failed', 1;"
    ' declare @order_hdrnumber int; select @order_hdrnumber = ord_hdrnumber from dbo.orderheader where ord_number = @ordnum;'
    " if @order_hdrnumber is null throw 50013, 'created TMW order could not be resolved', 1;"
  ))

  for ($stopIndex = 1; $stopIndex -le $stops.Count; $stopIndex++) {
    $freight = @($stops[$stopIndex - 1].freight)
    for ($freightIndex = 1; $freightIndex -le $freight.Count; $freightIndex++) {
      $sql.Add(" declare @created_fgt${stopIndex}_$freightIndex int = (select freight_number from @scope where stop_index = $stopIndex and freight_index = $freightIndex);")
      $sql.Add(" exec @ret = dbo.dx_add_refnumber_to_freight @created_fgt${stopIndex}_$freightIndex, 'SID', @supid${stopIndex}_$freightIndex;")
      $sql.Add(" if @ret < 1 throw 50025, 'could not add Pilot supplier contract reference', 1;")
      $sql.Add(" exec @ret = dbo.dx_add_refnumber_to_freight @created_fgt${stopIndex}_$freightIndex, 'SUN', @supname${stopIndex}_$freightIndex;")
      $sql.Add(" if @ret < 1 throw 50026, 'could not add Pilot supplier name reference', 1;")
      $sql.Add(" exec @ret = dbo.dx_add_refnumber_to_freight @created_fgt${stopIndex}_$freightIndex, 'PO', @oid${stopIndex}_$freightIndex;")
      $sql.Add(" if @ret < 1 throw 50027, 'could not add Pilot item PO reference', 1;")
    }
  }

  for ($stopIndex = 1; $stopIndex -le $stops.Count; $stopIndex++) {
    if (-not ($stops[$stopIndex - 1].address1 -or $stops[$stopIndex - 1].city)) {
      $sql.Add(" update s set cmp_name = @name$stopIndex, stp_arrivaldate = @arrival$stopIndex, stp_departuredate = @late$stopIndex, stp_origarrival = @early$stopIndex, stp_schdtlatest = @late$stopIndex, stp_timewindow = @window$stopIndex from dbo.stops s join (select distinct stop_number from @scope where stop_index = $stopIndex) x on x.stop_number = s.stp_number where s.ord_hdrnumber = @order_hdrnumber;")
      $sql.Add(" if @@rowcount <> 1 throw 50021, 'could not populate one Pilot stop', 1;")
      continue
    }
    $sql.Add(" declare @city_code$stopIndex int;")
    $sql.Add(" select @city_code$stopIndex = coalesce((select min(cty_code) from dbo.city where upper(rtrim(cty_name)) = upper(rtrim(@city_name$stopIndex)) and upper(rtrim(cty_state)) = upper(rtrim(@state$stopIndex)) and rtrim(cty_zip) = rtrim(@zip$stopIndex)), (select min(cty_code) from dbo.city where upper(rtrim(cty_name)) = upper(rtrim(@city_name$stopIndex)) and upper(rtrim(cty_state)) = upper(rtrim(@state$stopIndex))));")
    $sql.Add(" if @city_code$stopIndex is null throw 50020, 'Pilot stop city is absent from TMW', 1;")
    $sql.Add(" update s set cmp_name = @name$stopIndex, stp_address = @address1_$stopIndex, stp_address2 = @address2_$stopIndex, stp_city = @city_code$stopIndex, stp_state = @state$stopIndex, stp_zipcode = @zip$stopIndex, stp_arrivaldate = @arrival$stopIndex, stp_departuredate = @late$stopIndex, stp_origarrival = @early$stopIndex, stp_schdtlatest = @late$stopIndex, stp_timewindow = @window$stopIndex from dbo.stops s join (select distinct stop_number from @scope where stop_index = $stopIndex) x on x.stop_number = s.stp_number where s.ord_hdrnumber = @order_hdrnumber;")
    $sql.Add(" if @@rowcount <> 1 throw 50021, 'could not populate one Pilot stop address', 1;")
  }

  $stopCount = $stops.Count
  $sql.AddRange([string[]]@(
    " if exists (select 1 from @scope x join dbo.freightdetail f on f.fgt_number = x.freight_number where rtrim(isnull(f.fgt_supplier, 'UNKNOWN')) <> 'UNKNOWN') throw 50024, 'Pilot supplier must remain UNKNOWN for DX mapping', 1;"
    " if (select count(1) from dbo.stops s left join dbo.city c on c.cty_code = s.stp_city where s.ord_hdrnumber = @order_hdrnumber and nullif(rtrim(s.cmp_name), '') is not null and (rtrim(s.cmp_name) in ('NO TERMINAL', 'NO LOCATION') or (c.cty_code is not null and nullif(rtrim(s.stp_address), '') is not null and nullif(rtrim(s.stp_state), '') is not null and nullif(rtrim(s.stp_zipcode), '') is not null)) and s.stp_departuredate > s.stp_arrivaldate and s.stp_schdtlatest = s.stp_departuredate and ((s.stp_event = 'LUL' and rtrim(s.stp_timewindow) = 'Custom') or (s.stp_event = 'LLD' and nullif(rtrim(s.stp_timewindow), '') is null))) <> $stopCount throw 50022, 'created order has incomplete display stops or delivery windows', 1;"
    " update dbo.orderheader set ord_revtype1 = @division, ord_booked_revtype1 = @division where ord_hdrnumber = @order_hdrnumber;"
    " if @@rowcount <> 1 throw 50023, 'could not route Pilot order to its division', 1;"
    " exec @ret = dbo.dx_add_refnumber_to_order @ordnum, 'SCA', @scac;"
    " if @ret < 1 throw 50018, 'dx_add_refnumber_to_order failed for the SCAC', 1;"
    " if not exists (select 1 from dbo.orderheader where ord_hdrnumber = @order_hdrnumber and ord_order_source = 'EDI' and ord_editradingpartner = @partner and ord_edistate = 10 and ord_revtype1 = @division and rtrim(ord_bookedby) = 'DX') throw 50014, 'created order did not land in the Pilot accept queue', 1;"
    " if not exists (select 1 from dbo.referencenumber where ref_table = 'orderheader' and ref_tablekey = @order_hdrnumber and ref_type = 'SCA' and ref_number = @scac) throw 50019, 'created order has no exact SCAC reference', 1;"
    ' declare @archive_header_id bigint;'
    ' insert dbo.dx_Archive_header (dx_importid, dx_sourcename, dx_sourcedate, dx_accepted, dx_ordernumber, dx_orderhdrnumber, dx_movenumber, dx_doctype, dx_docnumber, dx_createdby, dx_createdate, dx_processed, dx_trpid, dx_billto)'
    " values ('dx_204', @source_name, @source_date, null, @ordnum, @order_hdrnumber, @mv, '204', '', 'PILOTFEED', @source_date, 'DONE', @partner, @billto);"
    ' set @archive_header_id = convert(bigint, scope_identity());'
  ))

  foreach ($row in @($plan.rows)) {
    $fieldSql = @{}
    foreach ($fieldName in @('dx_field001','dx_field002','dx_field003','dx_field004','dx_field005','dx_field006','dx_field007','dx_field008','dx_field009','dx_field013','dx_field014')) {
      $value = $row.fields[$fieldName]
      if ([string]$value -eq $script:OrderNumberToken) {
        $width = if ($fieldName -eq 'dx_field005') { 20 } else { 80 }
        $fieldSql[$fieldName] = "left(convert(varchar($width), @ordnum) + replicate(' ', $width), $width)"
      } elseif ($null -eq $value) {
        $fieldSql[$fieldName] = 'null'
      } else {
        $parameterName = '@d{0}_{1}' -f $row.sequence, $fieldName.Substring(3)
        [void]$parameters.Add((New-TmwParameter $parameterName 'VarChar' 200 ([string]$value)))
        $fieldSql[$fieldName] = $parameterName
      }
    }
    $stopSql = if ($row.stopIndex -gt 0) {
      "(select top (1) stop_number from @scope where stop_index = $($row.stopIndex) order by freight_index)"
    } else { 'null' }
    $freightSql = if ($row.freightIndex -gt 0) {
      "(select freight_number from @scope where stop_index = $($row.stopIndex) and freight_index = $($row.freightIndex))"
    } else { 'null' }
    $sql.Add((' insert dbo.dx_Archive_detail (dx_Archive_header_id, dx_seq, dx_stopnumber, dx_freightnumber, dx_manifeststop, dx_field001, dx_field002, dx_field003, dx_field004, dx_field005, dx_field006, dx_field007, dx_field008, dx_field009, dx_field013, dx_field014) ' +
      "select @archive_header_id, $($row.sequence), $stopSql, $freightSql, null, " +
      (@($fieldSql['dx_field001'], $fieldSql['dx_field002'], $fieldSql['dx_field003'], $fieldSql['dx_field004'],
         $fieldSql['dx_field005'], $fieldSql['dx_field006'], $fieldSql['dx_field007'], $fieldSql['dx_field008'],
         $fieldSql['dx_field009'], $fieldSql['dx_field013'], $fieldSql['dx_field014']) -join ', ') + ';'))
  }

  $detailCount = @($plan.rows).Count
  $sql.AddRange([string[]]@(
    " if (select count(1) from dbo.dx_Archive_detail where dx_Archive_header_id = @archive_header_id) <> $detailCount throw 50015, 'DX archive detail count mismatch', 1;"
    ' if @commit = 1 commit transaction; else rollback transaction;'
    " select cast(1 as bit) ok, case when @commit = 1 then 'CREATED' else 'VALIDATED' end action, cast(0 as bit) requires_review, @pilot_id pilot_order_id, @order_hdrnumber order_hdrnumber, rtrim(@ordnum) order_number, @archive_header_id archive_header_id, convert(tinyint, 10) edistate, 'AVL' order_status, $detailCount detail_rows, @fingerprint incoming_fingerprint, cast(null as varchar(64)) existing_fingerprint, db_name() database_name;"
    'end try'
    'begin catch'
    ' if @@trancount > 0 rollback transaction;'
    ' throw;'
    'end catch;'
  ))

  [pscustomobject]@{
    sql = $sql -join "`n"
    parameters = $parameters.ToArray()
    archive = $plan
  }
}

function Add-TmwSqlParameter {
  param($Command, $Definition)
  $sqlType = [System.Enum]::Parse([System.Data.SqlDbType], [string]$Definition.type)
  $parameter = if ($Definition.size -gt 0) {
    $Command.Parameters.Add($Definition.name, $sqlType, $Definition.size)
  } else {
    $Command.Parameters.Add($Definition.name, $sqlType)
  }
  $parameter.Value = if ($null -eq $Definition.value -or [string]$Definition.value -eq '') {
    if ($Definition.type -eq 'VarChar') { [string]$Definition.value } else { [DBNull]::Value }
  } else { $Definition.value }
}

function Invoke-TmwOrder {
  param(
    [Parameter(Mandatory)]$PilotOrder,
    [Parameter(Mandatory)]$Cfg,
    [string]$ConnectionString,
    [switch]$Commit
  )
  $commandPlan = New-TmwOrderCommand $PilotOrder $Cfg -Commit:$Commit
  if (-not $ConnectionString) { throw 'TMW connection string is required.' }

  $connection = [System.Data.SqlClient.SqlConnection]::new($ConnectionString)
  $command = $connection.CreateCommand()
  $command.CommandText = $commandPlan.sql
  $command.CommandTimeout = [int](Get-TmwValue $Cfg 'tmw' 'command_timeout_seconds' 120)
  foreach ($definition in $commandPlan.parameters) { Add-TmwSqlParameter $command $definition }
  try {
    $connection.Open()
    $reader = $command.ExecuteReader()
    try {
      $result = $null
      do {
        $names = @($(for ($i = 0; $i -lt $reader.FieldCount; $i++) { $reader.GetName($i) }))
        if ($names -contains 'action' -and $reader.Read()) {
          $result = [ordered]@{}
          foreach ($name in $names) {
            $value = $reader[$name]
            $result[$name] = if ($value -is [DBNull]) { $null } else { $value }
          }
        }
      } while ($reader.NextResult())
      if (-not $result) { throw 'TMW direct staging returned no result row.' }
      [pscustomobject]$result
    } finally {
      $reader.Dispose()
    }
  } finally {
    $command.Dispose()
    $connection.Dispose()
  }
}

function Invoke-TmwRetry {
  param(
    [Parameter(Mandatory)][scriptblock]$Operation,
    [int]$MaxAttempts = 3,
    [int]$DelaySeconds = 2
  )
  if ($MaxAttempts -lt 1) { throw 'MaxAttempts must be at least 1.' }
  for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
    try {
      $result = & $Operation
      $result | Add-Member -NotePropertyName attempts -NotePropertyValue $attempt -Force
      return $result
    } catch {
      if ($attempt -eq $MaxAttempts) { throw }
      Write-Warning "TMW attempt $attempt failed: $($_.Exception.Message). Retrying."
      Start-Sleep -Seconds ($DelaySeconds * $attempt)
    }
  }
}

function Write-TmwOrders {
  param(
    [Parameter(Mandatory)][AllowEmptyCollection()]$Orders,
    [Parameter(Mandatory)]$Cfg,
    [string]$ConnectionString,
    [switch]$Commit
  )
  $items = @($Orders)
  if (-not $items.Count) { return @() }
  $maxParallel = [Math]::Max(1, [Math]::Min(20, [int](Get-TmwValue $Cfg 'tmw' 'max_parallel' 10)))
  $maxAttempts = [Math]::Max(1, [int](Get-TmwValue $Cfg 'tmw' 'retry_attempts' 3))
  $retryDelay = [Math]::Max(0, [int](Get-TmwValue $Cfg 'tmw' 'retry_delay_seconds' 2))
  $modulePath = Join-Path $PSScriptRoot 'PilotCarrierIntegration.psd1'
  $cfgJson = $Cfg | ConvertTo-Json -Depth 20 -Compress
  $pool = [RunspaceFactory]::CreateRunspacePool(1, $maxParallel)
  $pool.ApartmentState = 'MTA'
  $pool.Open()
  $workers = [System.Collections.Generic.List[object]]::new()
  $workerScript = {
    param($Order, $CfgJson, $ConnectionString, $Commit, $ModulePath, $MaxAttempts, $RetryDelay)
    $workerCfg = $CfgJson | ConvertFrom-Json
    try {
      & (Import-Module $ModulePath -PassThru) {
        param($o, $c, $cs, $cm, $ma, $rd)
        Invoke-TmwRetry -MaxAttempts $ma -DelaySeconds $rd -Operation { Invoke-TmwOrder -PilotOrder $o -Cfg $c -ConnectionString $cs -Commit:$cm }
      } $Order $workerCfg $ConnectionString $Commit $MaxAttempts $RetryDelay
    } catch {
      [pscustomobject]@{
        ok = $false; action = 'FAILED'; pilot_order_id = [string]$Order.pilotOrderId
        requires_review = $false
        order_hdrnumber = $null; order_number = $null; archive_header_id = $null
        edistate = $null; detail_rows = 0; database_name = $null
        attempts = $MaxAttempts; error = $_.Exception.Message
      }
    }
  }
  try {
    foreach ($order in $items) {
      $pipeline = [PowerShell]::Create()
      [void]$pipeline.AddScript($workerScript)
      [void]$pipeline.AddArgument($order)
      [void]$pipeline.AddArgument($cfgJson)
      [void]$pipeline.AddArgument($ConnectionString)
      [void]$pipeline.AddArgument([bool]$Commit)
      [void]$pipeline.AddArgument($modulePath)
      [void]$pipeline.AddArgument($maxAttempts)
      [void]$pipeline.AddArgument($retryDelay)
      $pipeline.RunspacePool = $pool
      $workers.Add([pscustomobject]@{ pipeline = $pipeline; handle = $pipeline.BeginInvoke(); ref = $order.pilotOrderId })
    }
    while (@($workers | Where-Object { -not $_.handle.IsCompleted }).Count) {
      Start-Sleep -Milliseconds 100
    }
    @($workers | ForEach-Object {
      $worker = $_
      try {
        @($worker.pipeline.EndInvoke($worker.handle)) | Select-Object -Last 1
      } catch {
        [pscustomobject]@{
          ok = $false; action = 'FAILED'; pilot_order_id = [string]$worker.ref
          requires_review = $false
          attempts = $maxAttempts; error = $_.Exception.Message
        }
      } finally {
        $worker.pipeline.Dispose()
      }
    })
  } finally {
    $pool.Close()
    $pool.Dispose()
  }
}

# the TMW writer reads settings through PSObject.Properties, so the order path works on the JSON object shape
function ConvertTo-PilotOrderCfg([hashtable]$Settings) { $Settings | ConvertTo-Json -Depth 20 | ConvertFrom-Json }

# DataAgent source: Pilot orders ready to stage, translated
function Read-PilotCarrierOrders {
  param([hashtable]$Options)
  $session = New-PilotCarrierSession $Options.Settings.pilot
  $orders = @(Receive-PilotOrders (ConvertTo-PilotOrderCfg $Options.Settings) $session)
  Write-Log "Orders : $($orders.Count) from Pilot to stage"
  $orders
}

# DataAgent formatter: keep the translated orders, types intact, for the destination
function Save-PilotOrderPlan {
  param($Data, [hashtable]$Options)
  $null = New-Item -ItemType Directory -Force (Split-Path $Options.Path)
  @($Data) | Export-Clixml -LiteralPath $Options.Path -Depth 20
}

# DataAgent destination: stage each order in TMW; dry_run validates and rolls back
function Send-PilotOrderPlan {
  param([string]$Path, [hashtable]$Options)
  $orders = @(Import-Clixml -LiteralPath $Path)
  $cfg = ConvertTo-PilotOrderCfg $Options.Settings
  $commit = -not $Options.Settings.dry_run
  Write-Log "Stage : $($orders.Count) order(s), $(if ($commit) { 'commit' } else { 'validate only' })"
  $results = @(Write-TmwOrders -Orders $orders -Cfg $cfg -ConnectionString $cfg.tmw.connection_string -Commit:$commit)
  foreach ($result in $results) {
    if ($result.requires_review) { Write-Log ("Review : {0} {1}: TMW order {2}, status {3}, state {4}" -f $result.pilot_order_id, $result.action, $result.order_number, $result.order_status, $result.edistate) }
    elseif ($result.ok) { Write-Log ("{0,-7}: {1}: TMW order {2}, state {3}" -f $result.action, $result.pilot_order_id, $result.order_number, $result.edistate) }
    else { Write-Log ("Failed : {0} after {1} attempt(s): {2}" -f $result.pilot_order_id, $result.attempts, $result.error) }
  }
  $failed = @($results | Where-Object { -not $_.ok })
  if ($failed.Count) { throw "$($failed.Count) Pilot order(s) failed after retries; the next run tries again." }
}