Functional Weave
Code in Rust

hospitality.cancellation-charge@1.0.0

impl/typescript.ts

2,883 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 { daysBetween } from "./dates_days_between.ts";  ← from dates.days-between ^1.0.0 · built alongside by fune
import { type Money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { applyRate } from "./money_apply_rate.ts";  ← from money.apply-rate ^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 CancellationBand, type CancellationCharge } from "./hospitality_cancellation_charge_types.ts";

/**
 * The cancellation fee a policy sets for the notice given.
 *
 * Notice is counted in calendar days from the day of cancelling to the day of
 * arrival, so cancelling on 4 December for an 18 December arrival is 14 days.
 * The band with the most notice that the cancellation still meets applies. A
 * cancellation on or after the arrival day is 0 days' notice, the no-show band.
 */
export function cancellationCharge(
  policy: readonly CancellationBand[],
  nightlyRates: readonly Money[],
  arrival: string,
  cancelledOn: string,
): CancellationCharge {
  if (policy.length === 0) throw new RangeError("policy must have at least one band");
  if (nightlyRates.length === 0) throw new RangeError("nightlyRates must have at least one night");
  const seen = new Set<number>();
  for (const band of policy) {
    if (!Number.isInteger(band.minDaysNotice) || band.minDaysNotice < 0) {
      throw new RangeError(`minDaysNotice must not be negative, received ${band.minDaysNotice}`);
    }
    if (seen.has(band.minDaysNotice)) {
      throw new RangeError(`policy has two bands for ${band.minDaysNotice} days' notice`);
    }
    seen.add(band.minDaysNotice);
    if (band.nights !== null && (!Number.isInteger(band.nights) || band.nights < 1)) {
      throw new RangeError(`nights must be at least 1 or null for the whole stay, received ${band.nights}`);
    }
    if (!Number.isInteger(band.basisPoints) || band.basisPoints < 0 || band.basisPoints > 10000) {
      throw new RangeError(`basisPoints must be a whole number from 0 to 10000, received ${band.basisPoints}`);
    }
  }

  const daysNotice = daysBetween(cancelledOn, arrival);
  const effective = Math.max(daysNotice, 0);
  let chosen: CancellationBand | null = null;
  for (const band of policy) {
    if (band.minDaysNotice <= effective && (chosen === null || band.minDaysNotice > chosen.minDaysNotice)) {
      chosen = band;
    }
  }
  if (chosen === null) {
    throw new RangeError(`no band in the policy covers ${effective} days' notice`);
  }

  const chargedNights = chosen.nights === null ? nightlyRates.length : Math.min(chosen.nights, nightlyRates.length);
  const chargeBase = sumMoney(nightlyRates.slice(0, chargedNights), nightlyRates[0].currency);
  // The whole stay is checked for one currency, not only the nights charged.
  sumMoney(nightlyRates, nightlyRates[0].currency);
  return {
    daysNotice,
    minDaysNotice: chosen.minDaysNotice,
    chargedNights,
    chargeBase,
    basisPoints: chosen.basisPoints,
    charge: applyRate(chargeBase, chosen.basisPoints, "half-up"),
  };
}