Functional Weave
Code in TypeScript

retail.refund-calculate@1.0.0

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