impl/typescript.ts
2,285 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 { type 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 { sumMoney } from "./money_sum.ts"; ← from money.sum ^1.0.0 · built alongside by fune
import { type Coupon, type CouponLine, type CouponCheck } from "./retail_coupon_validate_types.ts";
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
function isEligible(coupon: Coupon, line: CouponLine): boolean {
const open = coupon.skus.length === 0 && coupon.categories.length === 0;
const listed = open || coupon.skus.includes(line.sku) || coupon.categories.includes(line.category);
const excluded = coupon.excludedSkus.includes(line.sku) || coupon.excludedCategories.includes(line.category);
return listed && !excluded;
}
/**
* Whether a coupon can be used on a basket, with every reason it cannot.
* ISO dates compare correctly as strings.
*/
export function validateCoupon(
coupon: Coupon,
lines: readonly CouponLine[],
customerUses: number,
onDate: string,
): CouponCheck {
if (!ISO_DATE.test(onDate)) throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
if (!Number.isInteger(customerUses) || customerUses < 0) {
throw new RangeError(`customerUses must not be negative, received ${customerUses}`);
}
const eligibleLines: number[] = [];
lines.forEach((line, index) => {
if (isEligible(coupon, line)) eligibleLines.push(index);
});
const reasons: string[] = [];
if (coupon.validFrom !== null && onDate < coupon.validFrom) reasons.push("not-yet-valid");
if (coupon.validTo !== null && onDate > coupon.validTo) reasons.push("expired");
if (coupon.maxUses !== null && coupon.timesUsed >= coupon.maxUses) reasons.push("usage-limit-reached");
if (coupon.maxUsesPerCustomer !== null && customerUses >= coupon.maxUsesPerCustomer) {
reasons.push("customer-limit-reached");
}
if (eligibleLines.length === 0) reasons.push("no-eligible-items");
if (coupon.minimumSpend !== null) {
const counted: Money[] =
coupon.minimumSpendOn === "eligible" ? eligibleLines.map((i) => lines[i].total) : lines.map((l) => l.total);
const spend = sumMoney(counted, coupon.minimumSpend.currency);
if (compareMoney(spend, coupon.minimumSpend) < 0) reasons.push("minimum-spend-not-met");
}
return { valid: reasons.length === 0, reasons, eligibleLines };
}