Private/EntryWriter.ps1

function Get-LedgerEntryAmountState {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]]$Rows
    )

    $sum = [decimal]0
    $firstExtra = $null
    foreach ($row in $Rows) {
        try { $amount = [decimal]$row.Amount }
        catch { throw "Row amount '$($row.Amount)' for account $($row.Account) is not a number." }
        if ($null -eq $firstExtra -and $amount -ne [Math]::Round($amount, 2)) {
            $firstExtra = $row
        }
        $sum += $amount
    }
    [PSCustomObject]@{ Sum = $sum; HasExtraDecimals = $null -ne $firstExtra; FirstExtra = $firstExtra }
}

function Assert-LedgerEntryAmounts {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [hashtable[]]$Rows,

        [switch]$LegacyReversal
    )

    $state = Get-LedgerEntryAmountState -Rows $Rows
    if ($state.HasExtraDecimals -and -not $LegacyReversal) {
        $row = $state.FirstExtra
        throw "Row amount $($row.Amount) for account $($row.Account) has more than two decimals. Round it to whole öre."
    }
    if ($state.Sum -ne 0 -and -not ($LegacyReversal -and [Math]::Round($state.Sum, 2) -eq 0)) {
        throw "Entry does not balance. Sum of rows: $($state.Sum) (must be 0)."
    }
    if ($LegacyReversal -and $state.HasExtraDecimals) {
        Write-Warning "Reversing a legacy verification with more than two decimals exactly (reversal sum: $($state.Sum)). No amounts or original files were rounded or changed."
    }
}

function Assert-LedgerEntryDate {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)] [string]$YearDir,
        [Parameter(Mandatory)] [string]$FiscalYear,
        [Parameter(Mandatory)] [datetime]$Date
    )

    if (-not (Test-Path -LiteralPath $YearDir -PathType Container)) {
        throw "Fiscal year not found: $FiscalYear"
    }
    $yearFile = Join-Path $YearDir 'year.txt'
    if (-not (Test-Path -LiteralPath $yearFile -PathType Leaf)) {
        throw "Invalid fiscal year - year.txt not found in: $FiscalYear"
    }
    Assert-LedgerFiscalYearOpen -YearDir $YearDir -FiscalYear $FiscalYear -Action 'add entries'
    $start = $null
    $end = $null
    foreach ($line in (Get-Content -LiteralPath $yearFile)) {
        if ($line -match '^StartDate:\s*(.+)$') {
            $parsed = [datetime]::MinValue
            if ([datetime]::TryParse($Matches[1].Trim(), [ref]$parsed)) { $start = $parsed }
        }
        elseif ($line -match '^EndDate:\s*(.+)$') {
            $parsed = [datetime]::MinValue
            if ([datetime]::TryParse($Matches[1].Trim(), [ref]$parsed)) { $end = $parsed }
        }
    }
    if (-not $start -or -not $end) {
        throw "Invalid fiscal year - StartDate/EndDate missing or unparseable in year.txt for $FiscalYear."
    }
    if ($Date -lt $start -or $Date -gt $end) {
        throw "Date $($Date.ToString('yyyy-MM-dd')) is outside fiscal year $FiscalYear ($($start.ToString('yyyy-MM-dd')) to $($end.ToString('yyyy-MM-dd')))."
    }
}

function Write-LedgerVerification {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [string]$JournalPath,
        [string]$FiscalYear,
        [Parameter(Mandatory)] [datetime]$Date,
        [Parameter(Mandatory)] [string]$Description,
        [Parameter(Mandatory)] [hashtable[]]$Rows,
        [string[]]$Attachment,
        [switch]$PassThru,

        # Only Add-LedgerReversal uses the old precision rule, on mirrored rows.
        [switch]$LegacyReversal
    )

    $Description = ConvertTo-LedgerTextField $Description
    $JournalPath = Resolve-LedgerJournalPath -JournalPath $JournalPath -SchemaCheck Write
    $FiscalYear = Resolve-LedgerFiscalYear -FiscalYear $FiscalYear -JournalPath $JournalPath
    $YearDir = Join-Path $JournalPath $FiscalYear
    Assert-LedgerEntryDate -YearDir $YearDir -FiscalYear $FiscalYear -Date $Date
    Assert-LedgerEntryAmounts -Rows $Rows -LegacyReversal:$LegacyReversal

    $KontoplanFile = Join-Path $JournalPath 'accounts.txt'
    if (Test-Path $KontoplanFile) {
        $ValidAccounts = @{}
        foreach ($Line in (Get-Content $KontoplanFile)) {
            if ($Line -match '^(\d+)\t') {
                $ValidAccounts[$Matches[1]] = $true
            }
        }
        foreach ($Row in $Rows) {
            if (-not $ValidAccounts.ContainsKey($Row.Account)) {
                throw "Account $($Row.Account) does not exist in chart of accounts."
            }
        }
    }

    if ($Attachment) {
        foreach ($attachmentPath in $Attachment) {
            if (-not (Test-Path $attachmentPath -PathType Leaf)) {
                throw "Attachment file not found: $attachmentPath"
            }
        }
    }

    $ExistingFiles = Get-ChildItem -Path $YearDir -Filter 'ver*.txt' -File -ErrorAction SilentlyContinue
    if ($ExistingFiles) {
        $MaxNum = $ExistingFiles |
            ForEach-Object { if ($_.BaseName -match '^ver(\d+)$') { [int]$Matches[1] } } |
            Measure-Object -Maximum |
            Select-Object -ExpandProperty Maximum
        $NextNum = $MaxNum + 1
    }
    else {
        $NextNum = 1
    }

    $FileName = 'ver' + $NextNum.ToString('0000') + '.txt'
    $FilePath = Join-Path $YearDir $FileName
    $Lines = @(
        "Date: $($Date.ToString('yyyy-MM-dd'))"
        "Description: $Description"
        ""
    )
    foreach ($Row in $Rows) {
        $line = "$($Row.Account)`t$($Row.Amount)"
        if ($Row.ContainsKey('Objects') -and $Row.Objects -and $Row.Objects.Count -gt 0) {
            foreach ($dimNum in $Row.Objects.Keys) {
                $dim = Get-LedgerDimension -JournalPath $JournalPath -DimensionNumber $dimNum
                if (-not $dim) {
                    throw "Dimension $dimNum does not exist."
                }
                $obj = Get-LedgerObject -JournalPath $JournalPath -DimensionNumber $dimNum -ObjectNumber $Row.Objects[$dimNum]
                if (-not $obj) {
                    throw "Object '$($Row.Objects[$dimNum])' does not exist in dimension $dimNum."
                }
            }
            $line += "`t$(Format-ObjectTag -Objects $Row.Objects)"
        }
        if ($Row.ContainsKey('Comment') -and $null -ne $Row.Comment) {
            $commentText = ([string]$Row.Comment) -replace "[`t`r`n]+", ' '
            $commentText = $commentText.Trim()
            if ($commentText) {
                $line += "`t$commentText"
            }
        }
        $Lines += $line
    }

    if (-not $PSCmdlet.ShouldProcess($FiscalYear, "Add verification $FileName")) {
        return
    }
    Set-LedgerFileContent -Path $FilePath -Value $Lines
    Register-LedgerIntegrityItem -YearDir $YearDir -Kind Verification -Key "$NextNum" -Path $FilePath

    $attached = @()
    if ($Attachment) {
        $attached = Add-LedgerAttachment -JournalPath $JournalPath -FiscalYear $FiscalYear `
            -VerificationNumber $NextNum -Path $Attachment -PassThru
    }
    if ($PassThru) {
        [PSCustomObject]@{
            VerificationNumber = $NextNum
            FiscalYear         = $FiscalYear
            Date               = $Date
            Description        = $Description
            Path               = $FilePath
            Attachments        = @($attached | ForEach-Object { $_.FileName })
        }
    }
}