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