use super::funejson::Value; use super::dates_days_between::days_between; use super::money_amount::{money_from_value, money_to_value, Money}; use super::money_apply_rate::apply_rate; use super::money_sum::sum_money; /// 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 = 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], ¤cy); // The whole stay is checked for one currency, not only the nights charged. sum_money(nightly_rates, ¤cy); 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 = args[0].as_arr().iter().map(cancellation_band_from_value).collect(); let rates: Vec = 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())) }