impl/typescript.ts
3,355 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 { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^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 { type SaleLine, type ReturnLine, type RefundLine, type Refund } from "./retail_refund_calculate_types.ts";
/**
* Refund returned units at what they were actually paid: the basket discount
* is shared across every sale line first, then each line refunds by the
* difference of cumulative shares, so a line fully returned in several visits
* refunds exactly what it cost.
*/
export function calculateRefund(
lines: readonly SaleLine[],
basketDiscount: Money,
returns: readonly ReturnLine[],
): Refund {
if (lines.length === 0) throw new RangeError("a sale needs at least one line");
const currency = lines[0].net.currency;
for (const line of lines) {
if (line.net.currency !== currency) throw new RangeError(`currency mismatch: ${currency} and ${line.net.currency}`);
if (line.quantity < 1) throw new RangeError(`quantity must be 1 or more, received ${line.quantity}`);
if (line.net.minor < 0) throw new RangeError(`net must not be negative, received ${line.net.minor}`);
if (line.returnedBefore < 0 || line.returnedBefore > line.quantity) {
throw new RangeError(`returnedBefore must be from 0 to ${line.quantity}, received ${line.returnedBefore}`);
}
}
if (basketDiscount.currency !== currency) {
throw new RangeError(`currency mismatch: ${currency} and ${basketDiscount.currency}`);
}
const basket = sumMoney(lines.map((l) => l.net), currency);
if (basketDiscount.minor < 0) throw new RangeError(`basketDiscount must not be negative, received ${basketDiscount.minor}`);
if (basketDiscount.minor > basket.minor) {
throw new RangeError(`basketDiscount ${basketDiscount.minor} is more than the lines' total ${basket.minor}`);
}
const shares =
basketDiscount.minor === 0
? lines.map(() => money(0, currency))
: allocate(basketDiscount, lines.map((l) => l.net.minor));
const paid = lines.map((l, i) => l.net.minor - shares[i].minor);
const seen = new Set<number>();
const refunded: RefundLine[] = returns.map((r) => {
if (!Number.isInteger(r.line) || r.line < 0 || r.line >= lines.length) {
throw new RangeError(`no line ${r.line} in the sale`);
}
if (seen.has(r.line)) throw new RangeError(`line ${r.line} is returned twice in one refund`);
seen.add(r.line);
if (!Number.isInteger(r.quantity) || r.quantity < 1) {
throw new RangeError(`return quantity must be 1 or more, received ${r.quantity}`);
}
const line = lines[r.line];
if (line.returnedBefore + r.quantity > line.quantity) {
throw new RangeError(
`cannot return ${r.quantity} of line ${r.line}: ${line.quantity} bought, ${line.returnedBefore} already returned`,
);
}
const before = roundDiv(paid[r.line] * line.returnedBefore, line.quantity, "half-up");
const after = roundDiv(paid[r.line] * (line.returnedBefore + r.quantity), line.quantity, "half-up");
return {
line: r.line,
sku: line.sku,
quantity: r.quantity,
paid: money(paid[r.line], currency),
amount: money(after - before, currency),
};
});
return { lines: refunded, total: sumMoney(refunded.map((r) => r.amount), currency) };
}