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