Public/Add-LedgerInvoiceInterest.ps1

<#
.SYNOPSIS
Calculates and books late-payment interest (dröjsmålsränta) on a posted, unpaid
customer invoice.

.DESCRIPTION
Books late-payment interest as an actual bookkeeping charge on the invoice: the
invoice's receivable account is debited and an interest income account is
credited with the interest amount (no VAT). The interest is appended to the
invoice's charges, which increases the invoice total and the open receivable, so
the accounts receivable continues to reconcile against the general ledger.

The interest amount is either supplied explicitly with -Amount, or calculated
from an annual rate as:

    interest = remaining amount * AnnualRate * daysOverdue / 365

where daysOverdue is the number of days from the invoice due date to the charge
date. Supply either -AnnualRate or -Amount.

Only a posted, unpaid invoice (status Booked or Partial) can be charged interest.

.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 invoice to charge interest on.

.PARAMETER AnnualRate
The annual interest rate as a fraction (e.g. 0.105 for 10.5 %). The interest is
calculated on the invoice's remaining amount for the number of days the invoice
is overdue. Ignored when -Amount is given.

.PARAMETER Amount
An explicit interest amount to book, overriding the calculation. Must be greater
than zero.

.PARAMETER Date
The date the interest is calculated to and booked. Defaults to today.

.PARAMETER Account
The interest income account. Defaults to '8310' (Ränteintäkter).

.PARAMETER FiscalYear
Optional. The fiscal year to post the verification into. If omitted, the fiscal
year containing the charge date is used.

.PARAMETER ChargeId
Optional stable identity for the interest charge. Supply the same ChargeId to
safely retry a call that may have been interrupted: completed interest is not
booked again, and an unfinished charge is completed with its originally planned
amount. Without a ChargeId every call books new interest, and an interrupted call
blocks further changes to the invoice until it is resumed with the ChargeId shown
in the error.

.PARAMETER PassThru
If specified, returns the updated invoice object.

.EXAMPLE
Add-LedgerInvoiceInterest -JournalPath .\MinFirma.ledger -InvoiceNumber 1 -AnnualRate 0.105 -Date '2024-06-01'

Calculates late-payment interest at 10.5 % on invoice 1's remaining amount for the
days it is overdue and books it (debit 1510, credit 8310).

.EXAMPLE
Add-LedgerInvoiceInterest -InvoiceNumber 1 -Amount 250

Books an explicit 250 kr interest charge on invoice 1.

.EXAMPLE
$chargeId = [guid]::NewGuid()
Add-LedgerInvoiceInterest -JournalPath .\Volvo.ledger -InvoiceNumber 12 -AnnualRate 0.105 -Date '2024-06-01' -ChargeId $chargeId

Books interest with a stable identity, so the same command can be repeated after
an interruption without booking the interest twice.
#>

function Add-LedgerInvoiceInterest {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'The shared charge writer performs ShouldProcess; common parameters are forwarded.')]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()]
        [string]$JournalPath,

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

        [Parameter()]
        [ValidateRange(0, [double]::MaxValue)]
        [decimal]$AnnualRate,

        [Parameter()]
        [decimal]$Amount,

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

        [Parameter()]
        [string]$Account = '8310',

        [Parameter()]
        [string]$FiscalYear,

        [Parameter()]
        [ValidateScript({ if ($_ -eq [guid]::Empty) { throw 'ChargeId must not be an empty GUID.' }; $true })]
        [guid]$ChargeId,

        [Parameter()]
        [switch]$PassThru
    )
    process {
        $parameters = @{} + $PSBoundParameters
        $parameters.Remove('PassThru')
        $parameters.JournalPath = Resolve-LedgerJournalPath -JournalPath $JournalPath -SchemaCheck Write
        if (-not (Add-LedgerInvoiceChargeInternal @parameters -Type 'Interest')) {
            return
        }

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