Private/FileWrite.ps1

# Atomic file-write helper.
#
# Writing a multi-line ledger record (verification, invoice, payslip, opening
# balance, journal metadata, ...) directly with Set-Content means that an error
# part-way through the write — a full disk, a crash, an I/O error — can leave the
# file truncated and corrupt, destroying the whole record. To make each write
# atomic, the content is first written to a temporary file in the same directory
# and then moved into place. Move-Item within a directory is a rename, so the
# destination path always refers to either the complete old file or the complete
# new file, never a half-written one.

function Set-LedgerFileContent {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Path,

        # The content to write, matching what would be piped to Set-Content:
        # a string, an array of strings (one per line) or $null/empty.
        [Parameter(Mandatory)]
        [AllowNull()]
        [AllowEmptyString()]
        [AllowEmptyCollection()]
        $Value
    )

    $dir = Split-Path -Parent $Path
    if (-not $dir) { $dir = '.' }

    $tempPath = Join-Path $dir ('.tmp_' + [guid]::NewGuid().ToString('N'))

    try {
        # Set-Content writes UTF-8 (no BOM in PowerShell 7) with the same line
        # handling the callers previously relied on.
        Set-Content -Path $tempPath -Value $Value -Encoding UTF8
        Move-Item -Path $tempPath -Destination $Path -Force
    }
    catch {
        if (Test-Path -LiteralPath $tempPath) {
            Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue
        }
        throw
    }
}