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_add_months::add_months; ← from dates.add-months ^1.0.0 · built alongside by fune
use super::dates_days_between::days_between; ← from dates.days-between ^1.0.0 · built alongside by fune
use super::finance_proration::prorate; ← from finance.proration ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, 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
/// The return premium when a policy is cancelled mid-term.
///
/// Pro rata keeps the premium for the days on cover. Short period keeps the
/// share the insurer's scale gives for how long cover ran ("not exceeding one
/// month: 20%"), and a period longer than every band keeps the whole premium.
/// A month on the scale is a calendar month from inception, not 30 days.
///
/// # Panics
/// Panics on a negative premium, dates out of order, a bad scale or a
/// minimum in another currency.
pub fn cancellation_return_premium(
premium: &Money,
inception_date: &str,
expiry_date: &str,
cancellation_date: &str,
basis: &str,
short_period_scale: &[ShortPeriodBand],
minimum_retained: Option<&Money>,
) -> CancellationRefund {
if premium.minor < 0 {
panic!("premium must not be negative, received {}", premium.minor);
}
let total_days = days_between(inception_date, expiry_date);
if total_days <= 0 {
panic!("expiryDate must be after inceptionDate, received {} to {}", inception_date, expiry_date);
}
let days_in_force = days_between(inception_date, cancellation_date);
if days_in_force < 0 || days_in_force > total_days {
panic!("cancellationDate must be from inceptionDate to expiryDate, received {}", cancellation_date);
}
let mut retained = match basis {
"pro-rata" => prorate(premium, total_days, days_in_force).used,
"short-period" => {
if short_period_scale.is_empty() {
panic!("a short-period cancellation needs a scale");
}
let mut share = 10000;
let mut found = false;
let mut previous = 0;
for band in short_period_scale {
if band.up_to < 1 {
panic!("upTo must be a whole number of at least 1, received {}", band.up_to);
}
if band.unit != "days" && band.unit != "months" {
panic!("unit must be days or months, received \"{}\"", band.unit);
}
let bp = band.retained_basis_points;
if !(0..=10000).contains(&bp) {
panic!("retainedBasisPoints must be from 0 to 10000, received {}", bp);
}
if bp < previous {
panic!("a short-period scale must not keep less for a longer period");
}
previous = bp;
if found {
continue;
}
let within = if band.unit == "days" {
days_in_force <= band.up_to
} else {
cancellation_date <= add_months(inception_date, band.up_to).as_str()
};
if within {
share = bp;
found = true;
}
}
apply_rate(premium, share, "half-up")
}
other => panic!("unknown basis \"{}\": use pro-rata or short-period", other),
};
let mut minimum_applied = false;
if let Some(minimum) = minimum_retained {
assert_same_currency(premium, minimum);
if minimum.minor < 0 {
panic!("minimumRetained must not be negative, received {}", minimum.minor);
}
// The insurer can never keep more than it was paid.
let floor = minimum.minor.min(premium.minor);
if floor > retained.minor {
retained = money(floor, &premium.currency);
minimum_applied = true;
}
}
let return_premium = money(premium.minor - retained.minor, &premium.currency);
CancellationRefund {
days_in_force,
total_days,
retained,
return_premium,
minimum_applied,
}
}
pub fn short_period_band_from_value(v: &Value) -> ShortPeriodBand {
if let Value::Float(f) = v.get("upTo") {
panic!("upTo must be a whole number of at least 1, received {}", f);
}
if let Value::Float(f) = v.get("retainedBasisPoints") {
panic!("retainedBasisPoints must be from 0 to 10000, received {}", f);
}
ShortPeriodBand {
up_to: v.get("upTo").as_i64(),
unit: v.get("unit").as_str().to_string(),
retained_basis_points: v.get("retainedBasisPoints").as_i64(),
}
}
pub fn cancellation_refund_to_value(r: &CancellationRefund) -> Value {
Value::obj(vec![
("daysInForce", Value::Int(r.days_in_force)),
("totalDays", Value::Int(r.total_days)),
("retained", money_to_value(&r.retained)),
("returnPremium", money_to_value(&r.return_premium)),
("minimumApplied", Value::Bool(r.minimum_applied)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let scale: Vec<ShortPeriodBand> = args[5].as_arr().iter().map(short_period_band_from_value).collect();
let minimum = if args[6].is_null() { None } else { Some(money_from_value(&args[6])) };
cancellation_refund_to_value(&cancellation_return_premium(
&money_from_value(&args[0]),
args[1].as_str(),
args[2].as_str(),
args[3].as_str(),
args[4].as_str(),
&scale,
minimum.as_ref(),
))
}