use super::funejson::Value; use super::dates_add_months::add_months; use super::dates_days_between::days_between; use super::finance_proration::prorate; use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money}; use super::money_apply_rate::apply_rate; /// 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 = 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(), )) }