finance.invoice.calculate@1.0.1
impl/typescript.ts
2,440 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 { lineTotal } from "./finance_invoice_line_total.ts"; ← from finance.invoice.line-total ^1.0.0 · built alongside by fune
import { addVat } from "./finance_tax_add_vat.ts"; ← from finance.tax.add-vat ^1.0.0 · built alongside by fune
import { addMoney } from "./money_add.ts"; ← from money.add ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { sumMoney } from "./money_sum.ts"; ← from money.sum ^1.0.0 · built alongside by fune
import { type InvoiceLine, type CalculatedLine, type TaxGroup, type Invoice } from "./finance_invoice_calculate_types.ts";
/**
* Calculate a complete invoice.
*
* Note what this function does not contain: no multiplication, no percentages,
* no rounding, no rate table. Each of those is a capability that is already
* pinned by its own vectors. What is left here is the part that is genuinely
* about invoices - the order of operations, and how tax is grouped.
*
* VAT is calculated per line and then grouped by rate, which keeps each line's
* tax reconcilable against that line.
*/
export function calculateInvoice(lines: readonly InvoiceLine[], jurisdiction: string, invoiceDate: string): Invoice {
if (lines.length === 0) {
throw new RangeError("an invoice needs at least one line");
}
const currency = lines[0].unitPrice.currency;
const calculated: CalculatedLine[] = lines.map((line) => {
const net = lineTotal(line.unitPrice, line.quantity, line.discountBasisPoints);
const vat = addVat(net, jurisdiction, line.taxCategory, invoiceDate);
return {
description: line.description,
net: vat.net,
tax: vat.tax,
gross: vat.gross,
basisPoints: vat.basisPoints,
};
});
const subtotal = sumMoney(calculated.map((l) => l.net), currency);
const taxTotal = sumMoney(calculated.map((l) => l.tax), currency);
// Grouped by rate and ordered by rate, so the summary block on a printed
// invoice is stable between runs and between languages.
const groups = new Map<number, { taxable: Money; tax: Money }>();
for (const line of calculated) {
const current = groups.get(line.basisPoints) ?? { taxable: money(0, currency), tax: money(0, currency) };
groups.set(line.basisPoints, {
taxable: addMoney(current.taxable, line.net),
tax: addMoney(current.tax, line.tax),
});
}
const taxBreakdown = [...groups.entries()]
.sort((a, b) => a[0] - b[0])
.map(([basisPoints, group]) => ({ basisPoints, taxable: group.taxable, tax: group.tax }));
return {
currency,
lines: calculated,
subtotal,
taxTotal,
total: addMoney(subtotal, taxTotal),
taxBreakdown,
};
}