Private/Save-NSPScanBridgeReceipt.ps1

function Save-NSPScanBridgeReceipt {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$PdfPath,
        [Parameter(Mandatory)][System.Collections.IDictionary]$Binding,
        [Parameter(Mandatory)][ValidateSet('Queued','Uploading','Uploaded','Review')][string]$State,
        [string]$RemoteId,
        [switch]$VerifiedReconciliation
    )
    Assert-NSPScanBridgeJobBinding -Binding $Binding
    $pdf = Get-Item -LiteralPath $PdfPath -ErrorAction Stop
    $id = [IO.Path]::GetFileNameWithoutExtension($pdf.Name)
    if ($id -notmatch '^[0-9a-f]{32}$') { throw 'Invalid job ID.' }
    $receiptPath = [IO.Path]::ChangeExtension($pdf.FullName, '.json')
    $previous = $null
    if (Test-Path -LiteralPath $receiptPath) {
        $previous = Read-NSPScanBridgeReceipt -PdfPath $pdf.FullName -ReceiptPath $receiptPath -ExpectedBinding $Binding
    }
    $hash = (Get-FileHash -LiteralPath $pdf.FullName -Algorithm SHA256).Hash
    if ($previous -and ($previous.JobId -ne $id -or $previous.Hash -ne $hash)) {
        throw 'Job receipt does not match the PDF.'
    }
    if ($previous) {
        $allowed = @{
            Queued = @('Queued','Uploading','Review')
            Uploading = @('Uploading','Uploaded','Review')
            Uploaded = @('Uploaded')
            Review = $(if ($VerifiedReconciliation) { @('Review','Uploaded') } else { @('Review') })
        }
        if (-not $allowed.ContainsKey([string]$previous.State) -or $State -notin $allowed[[string]$previous.State]) {
            throw 'Invalid receipt state transition.'
        }
        if ($previous.State -eq 'Uploaded' -and $RemoteId -ne $previous.RemoteId) {
            throw 'An acknowledged remote item ID cannot change.'
        }
    }
    if ($State -eq 'Uploaded' -and -not $RemoteId) {
        throw 'Uploaded state requires remote item ID.'
    }
    $receipt = @{
        Version = 2
        Destination = $Binding
        JobId = $id
        State = $State
        Hash = $hash
        Size = [long]$pdf.Length
        TimestampUtc = [DateTime]::UtcNow.ToString('o')
        RemoteId = $RemoteId
    }
    $temp = $receiptPath + '.new'
    try {
        [IO.File]::WriteAllText($temp, ($receipt | ConvertTo-Json -Depth 5 -Compress))
        if (Test-Path -LiteralPath $receiptPath) {
            [IO.File]::Replace($temp, $receiptPath, [NullString]::Value)
        } else {
            [IO.File]::Move($temp, $receiptPath)
        }
    } finally {
        if (Test-Path -LiteralPath $temp) { Remove-Item -LiteralPath $temp -Force }
    }
}