Functional Weave
Code in Rust

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,
  };
}