finance.credit-note.calculate@1.0.0
impl/typescript.ts
2,790 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { calculateInvoice } from "./finance_invoice_calculate.ts"; ← from finance.invoice.calculate ^1.0.0 · built alongside by fune
import { type Invoice, type InvoiceLine } from "./finance_invoice_calculate_types.ts";
import { lineTotal } from "./finance_invoice_line_total.ts"; ← from finance.invoice.line-total ^1.0.0 · built alongside by fune
import { money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type CreditRequest } from "./finance_credit_note_calculate_types.ts";
/**
* A credit note against an invoice, with VAT at the original tax point.
*
* Every credit becomes a negative invoice line and the whole note is
* calculated by finance.invoice.calculate at the original date, so the VAT
* rates, rounding and grouping are the invoice's own.
*/
export function calculateCreditNote(
originalLines: readonly InvoiceLine[],
credits: readonly CreditRequest[],
jurisdiction: string,
originalTaxPoint: string,
): Invoice {
if (credits.length === 0) {
throw new RangeError("a credit note needs at least one line");
}
const credited = originalLines.map(() => 0);
const lines: InvoiceLine[] = credits.map((credit) => {
const n = credit.line;
if (!Number.isInteger(n) || n < 1 || n > originalLines.length) {
throw new RangeError(`credit refers to line ${n}, but the invoice has ${originalLines.length} lines`);
}
const original = originalLines[n - 1];
if (original.quantity <= 0) {
throw new RangeError(`line ${n} of the invoice is not a sale that can be credited`);
}
if ((credit.quantity === null) === (credit.net === null)) {
throw new RangeError(`credit for line ${n} needs exactly one of quantity and net`);
}
let line: InvoiceLine;
if (credit.quantity !== null) {
if (!Number.isInteger(credit.quantity) || credit.quantity <= 0) {
throw new RangeError(`credit quantity for line ${n} must be greater than zero`);
}
line = { ...original, quantity: -credit.quantity };
} else {
const net = credit.net!;
if (net.currency !== original.unitPrice.currency) {
throw new RangeError(`credit for line ${n} must be in ${original.unitPrice.currency}`);
}
if (net.minor <= 0) {
throw new RangeError(`credit amount for line ${n} must be greater than zero`);
}
line = {
description: original.description,
unitPrice: money(-net.minor, net.currency),
quantity: 1,
discountBasisPoints: 0,
taxCategory: original.taxCategory,
};
}
credited[n - 1] -= lineTotal(line.unitPrice, line.quantity, line.discountBasisPoints).minor;
if (credited[n - 1] > lineTotal(original.unitPrice, original.quantity, original.discountBasisPoints).minor) {
throw new RangeError(`credits against line ${n} exceed what it was invoiced for`);
}
return line;
});
return calculateInvoice(lines, jurisdiction, originalTaxPoint);
}