Functional Weave
Code in Python

insurance.premium-proration@1.0.0

impl/rust.rs

5,471 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_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(),
    ))
}