Functional Weave
Code in Python

retail.basket-total@1.0.0

impl/typescript.ts

3,748 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 { addVat, type VatBreakdown } from "./finance_tax_add_vat.ts";  ← from finance.tax.add-vat ^1.0.0 · built alongside by fune
import { removeVat } from "./finance_tax_remove_vat.ts";  ← from finance.tax.remove-vat ^1.0.0 · built alongside by fune
import { allocate } from "./money_allocate.ts";  ← from money.allocate ^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 { applyPromotions, type Promotion } from "./retail_promotion_apply.ts";  ← from retail.promotion-apply ^1.0.0 · built alongside by fune
import { type BasketLine, type BasketLineTotal, type BasketVatGroup, type Basket } from "./retail_basket_total_types.ts";

/**
 * The checkout: promotions, then the basket discount shared across the lines,
 * then VAT per line on what the line really costs, then delivery.
 *
 * No arithmetic lives here; the order of operations is the whole function.
 */
export function basketTotal(
  lines: readonly BasketLine[],
  promotions: readonly Promotion[],
  basketDiscount: Money,
  delivery: Money,
  deliveryTaxCategory: string,
  pricesIncludeVat: boolean,
  jurisdiction: string,
  onDate: string,
): Basket {
  const priced = applyPromotions(
    lines.map((l) => ({ sku: l.sku, unitPrice: l.unitPrice, quantity: l.quantity })),
    promotions,
  );
  const currency = priced.subtotal.currency;
  for (const [name, amount] of [
    ["basketDiscount", basketDiscount],
    ["delivery", delivery],
  ] as const) {
    if (amount.currency !== currency) throw new RangeError(`currency mismatch: ${currency} and ${amount.currency}`);
    if (amount.minor < 0) throw new RangeError(`${name} must not be negative, received ${amount.minor}`);
  }
  if (basketDiscount.minor > priced.total.minor) {
    throw new RangeError(
      `basketDiscount ${basketDiscount.minor} is more than the basket's ${priced.total.minor} after promotions`,
    );
  }

  const afterPromotions = priced.lines.map((l) => l.net.minor);
  const shares =
    basketDiscount.minor === 0 ? afterPromotions.map(() => money(0, currency)) : allocate(basketDiscount, afterPromotions);
  const vat = (amount: Money, category: string): VatBreakdown =>
    pricesIncludeVat ? removeVat(amount, jurisdiction, category, onDate) : addVat(amount, jurisdiction, category, onDate);

  const rates: number[] = [];
  const totals: BasketLineTotal[] = lines.map((line, i) => {
    const v = vat(money(afterPromotions[i] - shares[i].minor, currency), line.taxCategory);
    rates.push(v.basisPoints);
    return {
      sku: line.sku,
      quantity: line.quantity,
      gross: priced.lines[i].gross,
      promotionDiscount: priced.lines[i].discount,
      basketDiscount: shares[i],
      net: v.net,
      tax: v.tax,
      total: v.gross,
    };
  });
  const deliveryVat = vat(delivery, deliveryTaxCategory);

  // Grouped by rate, lowest first, so the receipt's VAT summary is stable.
  const groups = new Map<number, { net: number; tax: number }>();
  const add = (bp: number, net: Money, tax: Money): void => {
    const g = groups.get(bp) ?? { net: 0, tax: 0 };
    groups.set(bp, { net: g.net + net.minor, tax: g.tax + tax.minor });
  };
  totals.forEach((t, i) => add(rates[i], t.net, t.tax));
  if (delivery.minor !== 0) add(deliveryVat.basisPoints, deliveryVat.net, deliveryVat.tax);
  const vatBreakdown: BasketVatGroup[] = [...groups.entries()]
    .sort((a, b) => a[0] - b[0])
    .map(([basisPoints, g]) => ({ basisPoints, net: money(g.net, currency), tax: money(g.tax, currency) }));

  return {
    lines: totals,
    promotions: priced.applied,
    delivery: deliveryVat,
    vatBreakdown,
    subtotal: priced.subtotal,
    promotionDiscount: priced.discount,
    basketDiscount,
    net: sumMoney([...totals.map((t) => t.net), deliveryVat.net], currency),
    tax: sumMoney([...totals.map((t) => t.tax), deliveryVat.tax], currency),
    total: sumMoney([...totals.map((t) => t.total), deliveryVat.gross], currency),
  };
}