Functional Weave
Code in Python

insurance.ipt@1.0.2

impl/rust.rs

3,965 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::insurance_ipt_data::{IPT_RATES, IPT_RATES_HISTORY};  ← this capability’s own data, compiled from data/ipt-rates.json into the same file by fune build
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn is_iso_date(value: &str) -> bool {
    let bytes = value.as_bytes();
    bytes.len() == 10
        && bytes[4] == b'-'
        && bytes[7] == b'-'
        && bytes
            .iter()
            .enumerate()
            .all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
}

/// Add UK Insurance Premium Tax to a net premium, or take it out of a gross
/// one, at the rate in force on the date the insurer receives the premium.
///
/// Only one figure is ever rounded - the tax when adding, the net when taking
/// out - and the other is the difference, so net + tax is always the gross.
///
/// # Panics
/// Panics on an unknown band, a malformed date, or a date no rate covers.
pub fn insurance_premium_tax(amount: &Money, includes_tax: bool, band: &str, on_date: &str, mode: &str) -> IptBreakdown {
    if band != "standard" && band != "higher" {
        panic!("unknown IPT band \"{}\": use standard or higher", band);
    }
    if !is_iso_date(on_date) {
        panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
    }
    let mut found: Option<(i64, &'static str)> = None;
    for r in IPT_RATES {
        if r.band != band || on_date < r.valid_from {
            continue;
        }
        if let Some(to) = r.valid_to {
            if on_date > to {
                continue;
            }
        }
        found = Some((r.basis_points, r.valid_from));
    }
    let (bp, valid_from) = match found {
        Some(f) => f,
        None => {
            // A history=current build keeps only the rates in force; answering
            // an older date with today's rate would be exactly the wrong answer.
            if IPT_RATES_HISTORY != "full" {
                let mut earliest: Option<&'static str> = None;
                for r in IPT_RATES {
                    if r.band == band && (earliest.is_none() || r.valid_from < earliest.unwrap()) {
                        earliest = Some(r.valid_from);
                    }
                }
                if let Some(earliest) = earliest {
                    if on_date < earliest {
                        panic!(
                            "no IPT rule for {} on {}: this build was installed with history={}, so it only carries rates from {}. Reinstall with history=full for older premiums.",
                            band, on_date, IPT_RATES_HISTORY, earliest
                        );
                    }
                }
            }
            panic!("no IPT rule for {} on {}", band, on_date)
        }
    };
    let (net, tax, gross) = if includes_tax {
        let net = money(round_div(amount.minor * 10000, 10000 + bp, mode), &amount.currency);
        let tax = money(amount.minor - net.minor, &amount.currency);
        (net, tax, amount.clone())
    } else {
        let tax = money(round_div(amount.minor * bp, 10000, mode), &amount.currency);
        let gross = money(amount.minor + tax.minor, &amount.currency);
        (amount.clone(), tax, gross)
    };
    IptBreakdown {
        net,
        tax,
        gross,
        basis_points: bp,
        band: band.to_string(),
        valid_from: valid_from.to_string(),
    }
}

pub fn ipt_breakdown_to_value(b: &IptBreakdown) -> Value {
    Value::obj(vec![
        ("net", money_to_value(&b.net)),
        ("tax", money_to_value(&b.tax)),
        ("gross", money_to_value(&b.gross)),
        ("basisPoints", Value::Int(b.basis_points)),
        ("band", Value::str(&b.band)),
        ("validFrom", Value::str(&b.valid_from)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    ipt_breakdown_to_value(&insurance_premium_tax(
        &money_from_value(&args[0]),
        args[1].as_bool(),
        args[2].as_str(),
        args[3].as_str(),
        args[4].as_str(),
    ))
}