JEI_Import_Documents-App/recon/fix_plan_apply.ps1
Mavis 5f73d2ea71 feat: Fedex import flow, orders + invoice APIs, payment bundles
- /fedex: batch create, document upload/replace/delete, save shipment, imported list
- /api/orders, /api/invoices, /api/pb-file, /api/pb/refresh-token, /api/stock-codes
- /api/payments/bundle: generate + download-all remittance bundles
- shared app/components/DocCell.tsx and app/lib/{invoice-docs,pb-fetch,pb-token}.ts
- recon: invoice reconciliation reports and PowerShell fix-plan scripts
- .gitignore: exclude one-off scripts/_* debugging artefacts, recon/pdfs+txts, data backups
2026-10-18 17:27:40 +02:00

178 lines
No EOL
7.5 KiB
PowerShell

# JEI Line Fix APPLY (destructive!)
# Reads recon\fix_plan.json (produced by fix_plan_dryrun.ps1) and:
# - DELETEs each record marked for deletion
# - CREATEs each new line for missing lines
# Plus:
# - Uploads the attached 165709.pdf to replace the 600-byte stub
#
# Usage: powershell -ExecutionPolicy Bypass -File recon\fix_plan_apply.ps1
$ErrorActionPreference = 'Stop'
$pbUrl = 'https://pb.agent.apphub.co.za'
$envPath = 'C:\Users\SJ\.minimax-agent\projects\JEI\.env.local'
$reconDir = 'C:\Users\SJ\.minimax-agent\projects\JEI\recon'
$attachedPdf = 'C:\Users\SJ\.minimax\v2\assets\2026\10\12\13-32-47-717-asset_20261012-133247-717_2646651ee030_f6d94428-InV CI877409722884.pdf'
$token = (Get-Content $envPath | Select-String -Pattern '^PB_TOKEN=' | ForEach-Object { $_ -replace '^PB_TOKEN=','' }).Trim()
$headers = @{ Authorization = "Bearer $token" }
# Cache stock codes for quick lookup
$codeIdCache = @{}
function Get-StockId($code) {
if ($codeIdCache.ContainsKey($code)) { return $codeIdCache[$code] }
$r = Invoke-RestMethod -Uri "$pbUrl/api/collections/JEI_stock_codes/records?filter=code=`"$code`"&fields=id" -Headers $headers -Method Get
if ($r.items.Count -eq 0) {
Write-Host " [WARN] stock code '$code' not found in master"
return $null
}
$codeIdCache[$code] = $r.items[0].id
return $r.items[0].id
}
# ----------------------------------------------------------------------
# 1. Replace 165709 stub PDF with the attached real PDF
# ----------------------------------------------------------------------
Write-Host "==== 1. Replacing 165709 stub PDF ===="
$inv165709 = Invoke-RestMethod -Uri "$pbUrl/api/collections/JEI_invoices/records?filter=invoice_no=`"165709`"&fields=id,pdf_file" -Headers $headers -Method Get
$recId = $inv165709.items[0].id
Write-Host " record id = $recId"
Write-Host " old pdf_file = $($inv165709.items[0].pdf_file)"
# PB file upload uses multipart/form-data. PowerShell 5.1 lacks -Form on Invoke-RestMethod,
# so use curl.exe which handles multipart correctly.
# IMPORTANT: field name must match the schema field name ('pdf_file'), not 'file'.
$tmpHeaders = New-TemporaryFile
& curl.exe -sS -L -X PATCH -H "Authorization: Bearer $token" -F "pdf_file=@$attachedPdf" "$pbUrl/api/collections/JEI_invoices/records/$recId" -D $tmpHeaders.FullName -o "$reconDir\165709_patch_resp.json"
$respStatus = Select-String -Path $tmpHeaders.FullName -Pattern '^HTTP' | Select-Object -First 1
Remove-Item $tmpHeaders.FullName -Force
Write-Host " HTTP: $($respStatus.Line.Trim())"
Write-Host " new pdf_file = $(([System.IO.File]::ReadAllText("$reconDir\165709_patch_resp.json") | ConvertFrom-Json).pdf_file)"
# ----------------------------------------------------------------------
# 2. Run delete + add plan from fix_plan.json
# ----------------------------------------------------------------------
Write-Host ""
Write-Host "==== 2. Reading fix_plan.json ===="
$plan = Get-Content "$reconDir\fix_plan.json" -Raw | ConvertFrom-Json
# Group entries by invoice
$byInvoice = @{}
foreach ($entry in $plan) {
$inv = $entry.invoice
if (-not $byInvoice.ContainsKey($inv)) { $byInvoice[$inv] = @() }
$byInvoice[$inv] += $entry
}
# Lookup invoice IDs
$invIdCache = @{}
function Get-InvId($invNo) {
if ($invIdCache.ContainsKey($invNo)) { return $invIdCache[$invNo] }
$r = Invoke-RestMethod -Uri "$pbUrl/api/collections/JEI_invoices/records?filter=invoice_no=`"$invNo`"&fields=id" -Headers $headers -Method Get
$invIdCache[$invNo] = $r.items[0].id
return $r.items[0].id
}
# Lookup line_no for adding new lines -- use max existing line_no + 1
function Get-NextLineNo($invId) {
$r = Invoke-RestMethod -Uri "$pbUrl/api/collections/JEI_invoice_lines/records?filter=invoice=`"$invId`"&perPage=500&fields=line_no" -Headers $headers -Method Get
$maxLine = 0
foreach ($x in $r.items) {
if ([int]$x.line_no -gt $maxLine) { $maxLine = [int]$x.line_no }
}
return $maxLine + 1
}
# Lookup commodity/country from PDF text by looking at all DB records for that code+qty
$pdfContextCache = @{}
function Get-PdfContext($invNo, $code, $qty) {
$key = "$invNo|$code|$qty"
if ($pdfContextCache.ContainsKey($key)) { return $pdfContextCache[$key] }
$txt = ''
$txtPath = "$reconDir\txts\$invNo.txt"
if (Test-Path $txtPath) {
$txt = [System.IO.File]::ReadAllText($txtPath, [System.Text.Encoding]::UTF8)
}
# Search for line: <qty> <code> ... <description>... [commodity: ...] [country: ...]
$re = "(?ms)^\s*$([regex]::Escape([string]$qty))\s+$([regex]::Escape($code))\s+(.+?)(?=^\s*\d+\.\d+\s+[A-Z]|^\s*Deliver To)"
$m = [regex]::Match($txt, $re)
$commodity = ''
$country = ''
if ($m.Success) {
$block = $m.Groups[3].Value
$cm = [regex]::Match($block, 'COMMODITY\s*CODE[:\s]+(\d+)')
if ($cm.Success) { $commodity = $cm.Groups[1].Value }
$cy = [regex]::Match($block, 'COUNTRY\s*OF\s*ORIGIN[:\s]+(\w{2,3})')
if ($cy.Success) { $country = $cy.Groups[1].Value }
}
$ctx = [pscustomobject]@{ commodity = $commodity; country = $country }
$pdfContextCache[$key] = $ctx
return $ctx
}
$totalDel = 0
$totalAdd = 0
foreach ($invNo in ($byInvoice.Keys | Sort-Object)) {
$entries = $byInvoice[$invNo]
$invId = Get-InvId $invNo
Write-Host ""
Write-Host "==== $invNo ===="
# Pass A: DELETEs
foreach ($e in $entries) {
foreach ($d in $e.deletes) {
$recId = $d.rec.id
Write-Host (" DELETE rec={0,-22} line_no={1,3} code={2,-22} qty={3,5} net={4,10} [{5}]" -f $recId, $d.rec.line_no, $d.rec.code, $d.rec.qty, $d.rec.net, $d.reason)
Invoke-RestMethod -Uri "$pbUrl/api/collections/JEI_invoice_lines/records/$recId" -Headers $headers -Method Delete | Out-Null
$totalDel++
}
}
# Pass B: ADDs
if (@($e.adds).Count -gt 0) {
$nextLine = Get-NextLineNo $invId
foreach ($a in $e.adds) {
$code = $a.code
$qty = [double]$a.qty
$stockId = Get-StockId $code
if (-not $stockId) {
Write-Host (" [SKIP-ADD] code='{0}' not in master" -f $code)
continue
}
$ctx = Get-PdfContext $invNo $code $qty
# Pull unit_price and net from an existing DB record that matches code+qty (best-effort)
$body = @{
invoice = $invId
line_no = $nextLine
stock_code = $stockId
quantity = $qty
unit_price = 0
net_amount = 0
commodity_code = $ctx.commodity
country_of_origin = $ctx.country
}
# Try to find unit_price and net_amount from existing DB record matching code+qty on this invoice
$existing = Invoke-RestMethod -Uri "$pbUrl/api/collections/JEI_invoice_lines/records?filter=invoice=`"$invId`"+AND+stock_code=`"$stockId`"&perPage=10" -Headers $headers -Method Get
foreach ($ex in $existing.items) {
if ([double]$ex.quantity -eq $qty) {
$body.unit_price = [double]$ex.unit_price
$body.net_amount = [double]$ex.net_amount
$body.commodity_code = if ($ex.commodity_code) { $ex.commodity_code } else { $ctx.commodity }
$body.country_of_origin = if ($ex.country_of_origin) { $ex.country_of_origin } else { $ctx.country }
break
}
}
Write-Host (" ADD line_no={0,3} code={1,-22} qty={2,5} country={3,-3} commodity={4}" -f $nextLine, $code, $qty, $body.country_of_origin, $body.commodity_code)
$created = Invoke-RestMethod -Uri "$pbUrl/api/collections/JEI_invoice_lines/records" -Headers $headers -Method Post -Body ($body | ConvertTo-Json) -ContentType "application/json"
$totalAdd++
$nextLine++
}
}
}
Write-Host ""
Write-Host "==== DONE ===="
Write-Host ("Total deletes: {0}" -f $totalDel)
Write-Host ("Total adds: {0}" -f $totalAdd)