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