Functional Weave
Code in Rust

retail.coupon-validate@1.0.1

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