Public/Add-LedgerCreditInvoice.ps1

<#
.SYNOPSIS
Creates a credit invoice (kreditfaktura) that reverses a posted customer invoice.

.DESCRIPTION
Creates a new credit-note invoice with negated rows for an existing, posted
invoice and books a reversing verification: the receivable account is credited
with the gross total, and revenue and VAT are debited back. Both the original
invoice and the new credit note are marked with status 'Credited' so the
accounts receivable reconciles to zero for the pair.

Only an invoice with status 'Booked' (posted but unpaid) can be credited:
- a 'Draft' invoice has no verification to reverse — post it first, or just delete it;
- an invoice with payments ('Partial'/'Paid') must have its payments handled first;
- an already 'Credited' invoice cannot be credited again.

.PARAMETER JournalPath
The path to an existing journal directory. If omitted, uses the current journal
set via Set-LedgerCurrentJournal.

.PARAMETER InvoiceNumber
The number of the posted invoice to credit.

.PARAMETER Date
The date of the credit note and its reversing verification. Defaults to today.

.PARAMETER FiscalYear
Optional. The fiscal year to post the reversal into. If omitted, the fiscal year
containing -Date is used.

.PARAMETER PassThru
If specified, returns the created credit-note invoice object. By default the
command produces no output.

.EXAMPLE
Add-LedgerCreditInvoice -JournalPath .\MinFirma.ledger -InvoiceNumber 1

Credits invoice 1, creating a credit note and a reversing verification such as:
  1510 Kundfordringar -12500
  3010 Försäljning +10000
  2610 Utgående moms +2500

.EXAMPLE
Add-LedgerCreditInvoice -InvoiceNumber 5 -Date '2024-04-30' -PassThru

Credits invoice 5 as of 30 April 2024 and returns the created credit note.
#>

function Add-LedgerCreditInvoice {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()]
        [string]$JournalPath,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [int]$InvoiceNumber,

        [Parameter()]
        [datetime]$Date = (Get-Date).Date,

        [Parameter()]
        [string]$FiscalYear,

        [Parameter()]
        [switch]$PassThru
    )
    process {
        $JournalPath = Resolve-LedgerJournalPath -JournalPath $JournalPath -SchemaCheck Write

        $invoiceDir = Get-LedgerInvoiceDirectory -JournalPath $JournalPath
        $filePath = Join-Path $invoiceDir (Get-LedgerInvoiceFileName -InvoiceNumber $InvoiceNumber)
        if (-not (Test-Path $filePath)) {
            throw "Invoice $InvoiceNumber does not exist."
        }

        $original = Read-LedgerInvoiceFile -Path $filePath

        switch ($original.Status) {
            'Booked' { }
            'Draft' { throw "Invoice $InvoiceNumber is not posted. Post it first with Invoke-LedgerInvoicePosting, or delete the draft." }
            'Credited' { throw "Invoice $InvoiceNumber has already been credited." }
            default { throw "Invoice $InvoiceNumber has payments (status '$($original.Status)') and cannot be credited. Handle the payments first." }
        }

        # Resolve the fiscal year from the credit-note date unless one was supplied.
        if (-not $FiscalYear) {
            $FiscalYear = Find-FiscalYearForDate -JournalPath $JournalPath -Date $Date
            if (-not $FiscalYear) {
                throw "No fiscal year covers the date $($Date.ToString('yyyy-MM-dd')). Create one with New-LedgerFiscalYear or specify -FiscalYear."
            }
        }

        # Build the reversing verification: negate the original posting.
        $accountSums = [ordered]@{}
        $addAmount = {
            param($account, $amount)
            if ($accountSums.Contains($account)) { $accountSums[$account] += $amount }
            else { $accountSums[$account] = $amount }
        }

        & $addAmount $original.ReceivableAccount (-$original.Total)
        foreach ($row in $original.Rows) {
            & $addAmount $row.Account $row.Amount
            if ($row.VatAmount -ne 0) {
                & $addAmount $row.VatAccount $row.VatAmount
            }
        }

        $entryRows = foreach ($account in $accountSums.Keys) {
            @{ Account = $account; Amount = [Math]::Round([decimal]$accountSums[$account], 2) }
        }

        # Allocate the next journal-wide invoice number for the credit note.
        $existing = Get-ChildItem -Path $invoiceDir -Filter 'inv*.txt' -File -ErrorAction SilentlyContinue
        if ($existing) {
            $maxNum = $existing |
                ForEach-Object { if ($_.BaseName -match '^inv(\d+)$') { [int]$Matches[1] } } |
                Measure-Object -Maximum |
                Select-Object -ExpandProperty Maximum
            $creditNumber = $maxNum + 1
        }
        else {
            $creditNumber = 1
        }

        $description = "Kreditfaktura $creditNumber - kreditering av faktura $InvoiceNumber"

        if (-not $PSCmdlet.ShouldProcess("Invoice $InvoiceNumber", "Create credit invoice $creditNumber")) {
            return
        }

        # Snapshot the original invoice and track the files this call creates so a
        # failure part-way through the multi-file booking can be rolled back, leaving
        # no half-applied credit (a booked verification without the paired invoice
        # status updates, or vice versa).
        $originalSnapshot = Get-Content -LiteralPath $filePath -Raw -Encoding UTF8
        $creditNotePath = Join-Path $invoiceDir (Get-LedgerInvoiceFileName -InvoiceNumber $creditNumber)
        $verification = $null

        try {
            $verification = Add-LedgerEntry -JournalPath $JournalPath -FiscalYear $FiscalYear `
                -Date $Date -Description $description -Rows @($entryRows) -PassThru

            # Persist the credit note with negated rows.
            $creditRows = foreach ($row in $original.Rows) {
                [PSCustomObject]@{
                    Account     = $row.Account
                    Amount      = - [decimal]$row.Amount
                    VatRate     = $row.VatRate
                    VatAccount  = $row.VatAccount
                    VatAmount   = - [decimal]$row.VatAmount
                    Description = $row.Description
                    Quantity    = if ($null -ne $row.Quantity) { - [decimal]$row.Quantity } else { $null }
                    Unit        = $row.Unit
                    UnitPrice   = $row.UnitPrice
                }
            }

            $creditNote = [PSCustomObject]@{
                InvoiceNumber      = $creditNumber
                CustomerNumber     = $original.CustomerNumber
                InvoiceDate        = $Date
                DueDate            = $Date
                Description        = "Kreditfaktura för faktura $InvoiceNumber"
                Status             = 'Credited'
                ReceivableAccount  = $original.ReceivableAccount
                BookedVerification = $verification.VerificationNumber
                BookedFiscalYear   = $FiscalYear
                Rows               = @($creditRows)
                Payments           = @()
                FilePath           = $creditNotePath
            }
            Save-LedgerInvoiceFile -Invoice $creditNote

            # Mark the original as credited so the receivable nets to zero.
            $original.Status = 'Credited'
            Save-LedgerInvoiceFile -Invoice $original
        }
        catch {
            # Undo any partial writes: remove the reversing verification and the
            # credit note, and restore the original invoice to its pre-call content.
            if ($verification) {
                $verPath = Join-Path (Join-Path $JournalPath $FiscalYear) ('ver' + $verification.VerificationNumber.ToString('0000') + '.txt')
                if (Test-Path -LiteralPath $verPath) {
                    Remove-Item -LiteralPath $verPath -Force -ErrorAction SilentlyContinue
                }
                Remove-LedgerIntegrityTail -YearDir (Join-Path $JournalPath $FiscalYear) -Kind Verification -Key "$($verification.VerificationNumber)"
            }
            if (Test-Path -LiteralPath $creditNotePath) {
                Remove-Item -LiteralPath $creditNotePath -Force -ErrorAction SilentlyContinue
            }
            Set-Content -LiteralPath $filePath -Value $originalSnapshot -NoNewline -Encoding UTF8
            throw
        }

        if ($PassThru) {
            Get-LedgerInvoice -JournalPath $JournalPath -InvoiceNumber $creditNumber
        }
    }
}