Private/YearClosing.ps1

# Recoverable fiscal-year closing. An immutable plan in <year>/closing.txt is written
# before the result verification and the status change, so an interrupted
# Close-LedgerFiscalYear can be completed by repeating it.

function Get-LedgerYearClosingPath {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)] [string]$JournalPath,
        [Parameter(Mandatory)] [string]$FiscalYear
    )
    Join-Path $JournalPath $FiscalYear 'closing.txt'
}

function Read-LedgerYearClosing {
    [CmdletBinding()]
    param ([Parameter(Mandatory)] [string]$Path)
    $fields = @{}
    foreach ($line in Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction Stop) {
        if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith(';')) { continue }
        if ($line -notmatch '^([A-Za-z]+):\t(.+)$' -or $fields.ContainsKey($Matches[1])) {
            throw "Invalid fiscal year closing record '$Path': malformed or duplicate field."
        }
        $fields[$Matches[1]] = $Matches[2]
    }
    $invariant = [Globalization.CultureInfo]::InvariantCulture
    $id = [guid]::Empty
    $number = -1
    $amount = [decimal]0
    $valid = $fields.FormatVersion -eq '1' -and
        [guid]::TryParseExact([string]$fields.OperationId, 'D', [ref]$id) -and $id -ne [guid]::Empty -and
        $fields.FiscalYear -eq (Split-Path (Split-Path $Path -Parent) -Leaf) -and
        [int]::TryParse([string]$fields.ResultVerification, [Globalization.NumberStyles]::None, $invariant, [ref]$number)
    if ($valid -and $fields.ContainsKey('ResultAmount')) {
        $valid = [decimal]::TryParse($fields.ResultAmount, [Globalization.NumberStyles]::AllowLeadingSign -bor [Globalization.NumberStyles]::AllowDecimalPoint, $invariant, [ref]$amount)
    }
    if ($valid -and $number -gt 0) {
        $valid = $fields.ContainsKey('ResultAmount') -and $amount -ne 0 -and
            $fields.ResultAccount -match '^\d+$' -and $fields.EquityAccount -match '^\d+$' -and
            $fields.VerificationHash -cmatch '^[0-9a-f]{64}$'
    }
    elseif ($valid) {
        $valid = -not ($fields.ContainsKey('ResultAccount') -or $fields.ContainsKey('EquityAccount') -or $fields.ContainsKey('VerificationHash')) -and
            (-not $fields.ContainsKey('ResultAmount') -or $amount -eq 0)
    }
    if (-not $valid) {
        throw "Invalid fiscal year closing record '$Path': missing or invalid identity, result or hash."
    }
    [pscustomobject]@{
        OperationId        = $id
        FiscalYear         = $fields.FiscalYear
        VerificationNumber = $number
        SkipResultEntry    = -not $fields.ContainsKey('ResultAmount')
        ResultAmount       = $amount
        ResultAccount      = $fields.ResultAccount
        EquityAccount      = $fields.EquityAccount
        VerificationHash   = $fields.VerificationHash
    }
}

function Save-LedgerYearClosing {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)] [string]$Path,
        [Parameter(Mandatory)] [psobject]$Operation
    )
    $lines = @(
        '; PSLedger fiscal year closing'
        "FormatVersion:`t1"
        "OperationId:`t$($Operation.OperationId)"
        "FiscalYear:`t$($Operation.FiscalYear)"
        "ResultVerification:`t$($Operation.VerificationNumber)"
    )
    if (-not $Operation.SkipResultEntry) {
        $lines += "ResultAmount:`t$(([decimal]$Operation.ResultAmount).ToString([Globalization.CultureInfo]::InvariantCulture))"
    }
    if ($Operation.VerificationNumber -gt 0) {
        $lines += "ResultAccount:`t$($Operation.ResultAccount)"
        $lines += "EquityAccount:`t$($Operation.EquityAccount)"
        $lines += "VerificationHash:`t$($Operation.VerificationHash)"
    }
    Set-LedgerFileContent -Path $Path -NoClobber -Value $lines
}

function Get-LedgerYearClosingEntry {
    # The result verification parameters derived from a closing plan.
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)] [string]$JournalPath,
        [Parameter(Mandatory)] [psobject]$Operation,
        [Parameter(Mandatory)] [datetime]$Date
    )
    @{
        JournalPath = $JournalPath; FiscalYear = $Operation.FiscalYear; Date = $Date
        Description = 'Årets resultat (bokslut)'; OperationId = $Operation.OperationId
        Rows = @(
            @{ Account = $Operation.ResultAccount; Amount = -$Operation.ResultAmount }
            @{ Account = $Operation.EquityAccount; Amount = $Operation.ResultAmount }
        )
    }
}