Functional Weave
Code in Rust

retail.loyalty-points@1.0.0

impl/typescript.ts

2,911 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 { subtractMoney } from "./money_add.ts";  ← from money.add ^1.0.0 · built alongside by fune
import { type Money, assertSameCurrency, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { compareMoney } from "./money_compare.ts";  ← from money.compare ^1.0.0 · built alongside by fune
import { type LoyaltyScheme, type LoyaltyOutcome } from "./retail_loyalty_points_types.ts";

/**
 * Redeem points against a purchase, then earn on the amount actually paid, at
 * the member's tier multiplier.
 */
export function loyaltyPoints(
  scheme: LoyaltyScheme,
  spend: Money,
  qualifyingSpend: Money,
  balance: number,
  redeemPoints: number,
): LoyaltyOutcome {
  for (const other of [scheme.earnPer, scheme.blockValue, qualifyingSpend, ...scheme.tiers.map((t) => t.threshold)]) {
    assertSameCurrency(spend, other);
  }
  if (spend.minor < 0) throw new RangeError(`spend must not be negative, received ${spend.minor}`);
  if (scheme.earnPer.minor <= 0) throw new RangeError(`earnPer must be more than 0, received ${scheme.earnPer.minor}`);
  if (scheme.earnPoints < 0) throw new RangeError(`earnPoints must not be negative, received ${scheme.earnPoints}`);
  if (scheme.redeemBlock < 1) throw new RangeError(`redeemBlock must be 1 or more, received ${scheme.redeemBlock}`);
  if (!Number.isInteger(balance) || balance < 0) throw new RangeError(`balance must not be negative, received ${balance}`);
  if (!Number.isInteger(redeemPoints) || redeemPoints < 0) {
    throw new RangeError(`redeemPoints must not be negative, received ${redeemPoints}`);
  }
  if (redeemPoints > balance) {
    throw new RangeError(`cannot redeem ${redeemPoints} points from a balance of ${balance}`);
  }
  if (redeemPoints % scheme.redeemBlock !== 0) {
    throw new RangeError(`points are redeemed in blocks of ${scheme.redeemBlock}, received ${redeemPoints}`);
  }

  const discount = money((redeemPoints / scheme.redeemBlock) * scheme.blockValue.minor, spend.currency);
  if (compareMoney(discount, spend) > 0) {
    throw new RangeError(
      `redeeming ${redeemPoints} points is worth ${discount.minor}, more than the spend of ${spend.minor}`,
    );
  }
  const amountPayable = subtractMoney(spend, discount);

  let tier: string | null = null;
  let multiplierBasisPoints = 10000;
  let best: Money | null = null;
  for (const t of scheme.tiers) {
    if (compareMoney(t.threshold, qualifyingSpend) > 0) continue;
    if (best === null || compareMoney(t.threshold, best) > 0) {
      best = t.threshold;
      tier = t.name;
      multiplierBasisPoints = t.multiplierBasisPoints;
    }
  }

  const base = roundDiv(amountPayable.minor * scheme.earnPoints, scheme.earnPer.minor, scheme.earnRounding);
  const earnedPoints = roundDiv(base * multiplierBasisPoints, 10000, scheme.earnRounding);

  return {
    tier,
    multiplierBasisPoints,
    redeemedPoints: redeemPoints,
    discount,
    amountPayable,
    earnedPoints,
    newBalance: balance - redeemPoints + earnedPoints,
  };
}