Bols.ps1
|
# Pilot BOL completions: correlate TMW completed freight to Pilot order items, plan, PUT /bol. function New-PilotBolPlan { param( [Parameter(Mandatory)][AllowEmptyCollection()]$TmwRows, [Parameter(Mandatory)][AllowEmptyCollection()]$PilotOrders, [Parameter(Mandatory)][AllowEmptyCollection()]$AvailableBols ) $ready = [Collections.Generic.List[object]]::new() $deferred = [Collections.Generic.List[object]]::new() foreach ($tmwOrder in @($TmwRows | Group-Object tmwOrderId)) { $refs = @($tmwOrder.Group.pilotOrderRef | Where-Object { $_ } | Sort-Object -Unique) if ($refs.Count -ne 1) { $deferred.Add([pscustomobject]@{ tmwOrderId = [long]$tmwOrder.Name reason = if ($refs.Count) { 'multiple Pilot PO references' } else { 'no Pilot PO reference' } }) continue } $pilotId = 0L if (-not [long]::TryParse([string]$refs[0], [ref]$pilotId) -or $pilotId -le 0) { $deferred.Add([pscustomobject]@{ tmwOrderId = [long]$tmwOrder.Name reason = 'Pilot PO reference is not a positive numeric dispatchOrderId' }) continue } $rows = @( $tmwOrder.Group | Group-Object freightId | ForEach-Object { @($_.Group | Sort-Object freightSequence)[0] } | Sort-Object freightSequence, freightId ) $freightIds = @($rows.freightId | Sort-Object -Unique) $errors = [Collections.Generic.List[string]]::new() $normalized = [Collections.Generic.List[object]]::new() $pilotOrder = @($PilotOrders | Where-Object { [long]$_.dispatchOrderId -eq $pilotId }) if ($pilotOrder.Count -ne 1) { $errors.Add("Pilot order correlation found $($pilotOrder.Count) orders") } $expectedItems = if ($pilotOrder.Count -eq 1) { @($pilotOrder[0].dispatchOrderItems | Where-Object { -not $_.isDeleted }) } else { @() } if (-not $errors.Count -and $expectedItems.Count -ne $freightIds.Count) { $errors.Add("Pilot expects $($expectedItems.Count) active items; TMW has $($freightIds.Count) freight lines") } $usedSourceBols = [Collections.Generic.HashSet[string]]::new() foreach ($row in $rows) { $valid = $true $itemId = 0L $bol = 0L if (-not [long]::TryParse([string]$row.bolNumber, [ref]$bol) -or $bol -le 0) { $errors.Add("freight $($row.freightId): invalid BOL") $valid = $false } if ($null -eq $row.grossGallons -or [decimal]$row.grossGallons -le 0) { $errors.Add("freight $($row.freightId): missing gross gallons") $valid = $false } if ($null -eq $row.netGallons -or [decimal]$row.netGallons -le 0) { $errors.Add("freight $($row.freightId): missing net gallons") $valid = $false } if (-not $row.startPullDateTime) { $errors.Add("freight $($row.freightId): missing pull start") $valid = $false } if (-not $row.endPullDateTime) { $errors.Add("freight $($row.freightId): missing pull end") $valid = $false } if (-not $row.dropDateTime) { $errors.Add("freight $($row.freightId): missing drop time") $valid = $false } if ([long]::TryParse([string]$row.pilotOrderItemId, [ref]$itemId) -and $itemId -gt 0) { $activeItemIds = @($expectedItems.dispatchOrderItemId | ForEach-Object { [long]$_ }) if ($expectedItems.Count -and $itemId -notin $activeItemIds) { $errors.Add("freight $($row.freightId): Pilot order item ID is not active on the order") $valid = $false } } elseif ($valid -and $expectedItems.Count) { $ranked = @($AvailableBols | Where-Object { [long]$_.orderNumber -eq $pilotId -and [long]$_.bolNumber -eq $bol -and -not $usedSourceBols.Contains(('{0}/{1}' -f $_.product360Id, $_.bolNumber)) } | ForEach-Object { [pscustomobject]@{ source = $_ grossDifference = [math]::Abs([decimal]$_.grossGallons - [decimal]$row.grossGallons) netDifference = [math]::Abs([decimal]$_.netGallons - [decimal]$row.netGallons) } } | Where-Object { $_.grossDifference -le 2 -and $_.netDifference -le 2 } | Sort-Object @{ Expression = { $_.grossDifference + $_.netDifference } }) if (-not $ranked.Count) { $errors.Add("freight $($row.freightId): no exact available-BOL product match") $valid = $false } elseif ($ranked.Count -gt 1 -and ($ranked[0].grossDifference + $ranked[0].netDifference) -eq ($ranked[1].grossDifference + $ranked[1].netDifference)) { $errors.Add("freight $($row.freightId): ambiguous available-BOL product match") $valid = $false } else { $source = $ranked[0].source $itemMatches = @($expectedItems | Where-Object { [long]$_.productSystemId -eq [long]$source.product360Id -or [long]$_.wraProductSystemId -eq [long]$source.product360Id }) if ($itemMatches.Count -ne 1) { $errors.Add("freight $($row.freightId): product $($source.product360Id) maps to $($itemMatches.Count) Pilot items") $valid = $false } else { $itemId = [long]$itemMatches[0].dispatchOrderItemId [void]$usedSourceBols.Add(('{0}/{1}' -f $source.product360Id, $source.bolNumber)) } } } else { $errors.Add("freight $($row.freightId): invalid Pilot order item ID") $valid = $false } if ($valid) { $normalized.Add([pscustomobject]@{ freightId = [long]$row.freightId freightSequence = [int]$row.freightSequence dispatchOrderItemId = [long]$itemId billOfLadingNumber = [long]$bol grossGallons = [decimal]$row.grossGallons netGallons = [decimal]$row.netGallons startPullDateTime = [datetime]$row.startPullDateTime endPullDateTime = [datetime]$row.endPullDateTime dropDateTime = [datetime]$row.dropDateTime railCarNumber = if ($row.railCarNumber) { [string]$row.railCarNumber } else { $null } }) } } $itemIds = @($normalized.dispatchOrderItemId | Sort-Object -Unique) if (-not $errors.Count -and $itemIds.Count -ne $freightIds.Count) { $errors.Add('Pilot order item IDs are not one-to-one with TMW freight lines') } $expectedItemIds = @($expectedItems.dispatchOrderItemId | ForEach-Object { [long]$_ } | Sort-Object -Unique) if (-not $errors.Count -and @($expectedItemIds | Where-Object { $_ -notin $itemIds }).Count) { $errors.Add('Resolved TMW freight does not cover every expected Pilot order item') } if ($errors.Count) { $deferred.Add([pscustomobject]@{ tmwOrderId = [long]$tmwOrder.Name dispatchOrderId = [long]$pilotId reason = (@($errors | Sort-Object -Unique) -join '; ') }) continue } $payload = [Collections.Generic.List[object]]::new() foreach ($row in $normalized) { $payload.Add([pscustomobject][ordered]@{ grossGallons = $row.grossGallons netGallons = $row.netGallons billOfLadingNumber = $row.billOfLadingNumber dispatchOrderId = [long]$pilotId dispatchOrderItemId = $row.dispatchOrderItemId dropDateTime = $row.dropDateTime.ToString('yyyy-MM-ddTHH:mm:ss') startPullDateTime = $row.startPullDateTime.ToString('yyyy-MM-ddTHH:mm:ss') endPullDateTime = $row.endPullDateTime.ToString('yyyy-MM-ddTHH:mm:ss') railCarNumber = $row.railCarNumber }) } $ready.Add([pscustomobject]@{ key = ('{0}/{1}' -f $pilotId, (@($payload | ForEach-Object { '{0}:{1}' -f $_.dispatchOrderItemId, $_.billOfLadingNumber }) -join ',')) tmwOrderId = [long]$tmwOrder.Name dispatchOrderId = [long]$pilotId freightLines = $freightIds.Count payload = @($payload) }) } [pscustomobject]@{ ready = @($ready) deferred = @($deferred) } } # Pilot orders around each TMW order date, plus Pilot's available BOLs for the legacy product match function Get-PilotBolContext { param([Parameter(Mandatory)][AllowEmptyCollection()]$TmwRows, [Parameter(Mandatory)]$Session) $ordersById = @{} $dates = @($TmwRows.orderDate | Where-Object { $_ } | ForEach-Object { ([datetime]$_).Date } | Sort-Object -Unique) if (-not $dates.Count) { throw 'TMW BOL candidates have no order dates for Pilot correlation.' } foreach ($date in $dates) { foreach ($order in @(Get-PilotOrder -Session $Session -StartDate $date.AddDays(-1) -EndDate $date.AddDays(2))) { $ordersById[[string]$order.dispatchOrderId] = $order } } [pscustomobject]@{ orders = @($ordersById.Values); availableBols = @(Get-PilotBol -Session $Session) } } # one order's completion; Pilot status 4 is the acknowledgement, its status text can be stale function Send-PilotBol { param([Parameter(Mandatory)]$Batch, [Parameter(Mandatory)]$Session) $response = Set-PilotBol -Session $Session -Payload $Batch.payload if ($response.status -ne 'success') { throw "Pilot BOL update failed for $($Batch.key): $($response.status)" } $returned = $response.data.payload if ([long]$returned.dispatchOrderId -ne [long]$Batch.dispatchOrderId) { throw "Pilot BOL response order mismatch for $($Batch.key)." } if ([int]$returned.dispatchOrderStatusTypeId -ne 4) { throw "Pilot BOL response did not Kiosk order $($Batch.dispatchOrderId)." } [int]$returned.dispatchOrderStatusTypeId } # DataAgent formatter: TMW rows in, plan file out function Save-PilotBolPlan { param($Data, [hashtable]$Options) $session = New-PilotCarrierSession $Options.Pilot $rows = @($Data) $context = Get-PilotBolContext -TmwRows $rows -Session $session $plan = New-PilotBolPlan -TmwRows $rows -PilotOrders $context.orders -AvailableBols $context.availableBols Write-Log "Orders : $(@($rows | Group-Object tmwOrderId).Count) completed in TMW, $(@($plan.ready).Count) ready, $(@($plan.deferred).Count) deferred" foreach ($item in @($plan.deferred)) { Write-Log "Waiting: TMW $($item.tmwOrderId): $($item.reason)" } $null = New-Item -ItemType Directory -Force (Split-Path $Options.Path) [pscustomobject]@{ ready = @($plan.ready); deferred = @($plan.deferred) } | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $Options.Path } # DataAgent destination: send every ready order, in parallel; failures retry in the next overlapping window function Send-PilotBolPlan { param([string]$Path, [hashtable]$Options) $ready = @((Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json).ready) if (-not $ready.Count) { Write-Log 'Nothing ready to send'; return } $session = New-PilotCarrierSession $Options.Pilot $null = Get-PilotToken $session $module = Join-Path $PSScriptRoot 'PilotCarrierIntegration.psd1' $throttle = if ($Options.ThrottleLimit) { [int]$Options.ThrottleLimit } else { 8 } $results = @($ready | ForEach-Object -Parallel { $batch = $_ try { $status = & (Import-Module $using:module -PassThru) { param($b, $s) Send-PilotBol -Batch $b -Session $s } $batch $using:session [pscustomobject]@{ ok = $true; batch = $batch; status = $status; error = $null } } catch { [pscustomobject]@{ ok = $false; batch = $batch; status = $null; error = $_.Exception.Message } } } -ThrottleLimit $throttle) foreach ($result in $results) { $bols = @($result.batch.payload.billOfLadingNumber) -join ',' if ($result.ok) { Write-Log "Sent : TMW $($result.batch.tmwOrderId), Pilot $($result.batch.dispatchOrderId), BOL $bols, status $($result.status)" } else { Write-Log "Failed : TMW $($result.batch.tmwOrderId), Pilot $($result.batch.dispatchOrderId): $($result.error)" } } $failed = @($results | Where-Object { -not $_.ok }) if ($failed.Count) { throw "$($failed.Count) Pilot BOL request(s) failed; the next run retries them." } } |