Functional Weave
Code in TypeScript

hospitality.cancellation-charge@1.0.0

impl/rust.rs

4,164 bytes · the Rust 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_days_between::days_between;  ← from dates.days-between ^1.0.0 · built alongside by fune
use super::money_amount::{money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_apply_rate::apply_rate;  ← from money.apply-rate ^1.0.0 · built alongside by fune
use super::money_sum::sum_money;  ← from money.sum ^1.0.0 · built alongside by fune

/// 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.
///
/// # Panics
/// Panics on an empty or malformed policy, no nights, a bad date, mixed
/// currencies, or when no band covers the notice given.
pub fn cancellation_charge(
    policy: &[CancellationBand],
    nightly_rates: &[Money],
    arrival: &str,
    cancelled_on: &str,
) -> CancellationCharge {
    if policy.is_empty() {
        panic!("policy must have at least one band");
    }
    if nightly_rates.is_empty() {
        panic!("nightlyRates must have at least one night");
    }
    let mut seen: Vec<i64> = Vec::new();
    for band in policy {
        if band.min_days_notice < 0 {
            panic!("minDaysNotice must not be negative, received {}", band.min_days_notice);
        }
        if seen.contains(&band.min_days_notice) {
            panic!("policy has two bands for {} days' notice", band.min_days_notice);
        }
        seen.push(band.min_days_notice);
        if let Some(n) = band.nights {
            if n < 1 {
                panic!("nights must be at least 1 or null for the whole stay, received {}", n);
            }
        }
        if !(0..=10000).contains(&band.basis_points) {
            panic!(
                "basisPoints must be a whole number from 0 to 10000, received {}",
                band.basis_points
            );
        }
    }

    let days_notice = days_between(cancelled_on, arrival);
    let effective = days_notice.max(0);
    let mut chosen: Option<&CancellationBand> = None;
    for band in policy {
        if band.min_days_notice <= effective
            && chosen.map_or(true, |c| band.min_days_notice > c.min_days_notice)
        {
            chosen = Some(band);
        }
    }
    let chosen = match chosen {
        Some(c) => c,
        None => panic!("no band in the policy covers {} days' notice", effective),
    };

    let stay = nightly_rates.len() as i64;
    let charged_nights = match chosen.nights {
        None => stay,
        Some(n) => n.min(stay),
    };
    let currency = nightly_rates[0].currency.clone();
    let charge_base = sum_money(&nightly_rates[..charged_nights as usize], &currency);
    // The whole stay is checked for one currency, not only the nights charged.
    sum_money(nightly_rates, &currency);
    CancellationCharge {
        days_notice,
        min_days_notice: chosen.min_days_notice,
        charged_nights,
        charge: apply_rate(&charge_base, chosen.basis_points, "half-up"),
        charge_base,
        basis_points: chosen.basis_points,
    }
}

pub fn cancellation_band_from_value(v: &Value) -> CancellationBand {
    CancellationBand {
        min_days_notice: v.get("minDaysNotice").as_i64(),
        nights: if v.get("nights").is_null() { None } else { Some(v.get("nights").as_i64()) },
        basis_points: v.get("basisPoints").as_i64(),
    }
}

pub fn cancellation_charge_to_value(c: &CancellationCharge) -> Value {
    Value::obj(vec![
        ("daysNotice", Value::Int(c.days_notice)),
        ("minDaysNotice", Value::Int(c.min_days_notice)),
        ("chargedNights", Value::Int(c.charged_nights)),
        ("chargeBase", money_to_value(&c.charge_base)),
        ("basisPoints", Value::Int(c.basis_points)),
        ("charge", money_to_value(&c.charge)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let policy: Vec<CancellationBand> = args[0].as_arr().iter().map(cancellation_band_from_value).collect();
    let rates: Vec<Money> = args[1].as_arr().iter().map(money_from_value).collect();
    cancellation_charge_to_value(&cancellation_charge(&policy, &rates, args[2].as_str(), args[3].as_str()))
}