Functional Weave
Code in Rust

property.deposit-cap@1.0.0

impl/rust.rs

4,016 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::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::property_deposit_cap_data::{DepositCapRule, DEPOSIT_CAPS, DEPOSIT_CAPS_HISTORY};  ← this capability’s own data, compiled from data/deposit-caps.json into the same file by fune build

// Keeps annual rent × 6 exact as a JavaScript number; the same limit here.
const MAX_MINOR: i64 = 1_000_000_000_000;

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

fn find_rule(jurisdiction: &str, on_date: &str) -> &'static DepositCapRule {
    let mut best: Option<&'static DepositCapRule> = None;
    let mut earliest: Option<&'static str> = None;
    for rule in DEPOSIT_CAPS {
        if rule.jurisdiction != jurisdiction {
            continue;
        }
        if earliest.map_or(true, |e| rule.valid_from < e) {
            earliest = Some(rule.valid_from);
        }
        if on_date < rule.valid_from {
            continue;
        }
        if let Some(to) = rule.valid_to {
            if on_date > to {
                continue;
            }
        }
        if best.map_or(true, |b| rule.valid_from > b.valid_from) {
            best = Some(rule);
        }
    }
    if let Some(rule) = best {
        return rule;
    }
    // A build installed with a pruned history must not answer an old date
    // with today's rule, so it says what happened instead.
    if DEPOSIT_CAPS_HISTORY != "full" {
        if let Some(e) = earliest {
            if on_date < e {
                panic!(
                    "no deposit cap rule for {} on {}: this build was installed with history={}, so it only carries rules from {}",
                    jurisdiction, on_date, DEPOSIT_CAPS_HISTORY, e
                );
            }
        }
    }
    panic!("no deposit cap rule for {} on {}", jurisdiction, on_date)
}

/// The maximum tenancy deposit: five weeks' rent below the annual-rent
/// threshold, six at or above it. The threshold is tested on the exact annual
/// rent, and the cap is annual rent × weeks ÷ 52 rounded down once, so it can
/// never sit a fraction of a penny over the statutory limit.
///
/// # Panics
/// Panics on a malformed date, rent not in GBP or negative, an unknown rent
/// period, or no rule for the jurisdiction on that date.
pub fn deposit_cap(rent: &Money, rent_period: &str, jurisdiction: &str, on_date: &str) -> DepositCap {
    if !is_iso_date(on_date) {
        panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
    }
    if rent.currency != "GBP" {
        panic!("rent must be in GBP, received {}", rent.currency);
    }
    if rent.minor < 0 {
        panic!("rent must not be negative, received {}", rent.minor);
    }
    let annual = match rent_period {
        "weekly" => rent.minor.checked_mul(52),
        "monthly" => rent.minor.checked_mul(12),
        "annual" => Some(rent.minor),
        other => panic!("unknown rent period \"{}\"", other),
    };
    let annual = match annual {
        Some(a) if a <= MAX_MINOR => a,
        _ => panic!("annual rent must be at most {} minor units", MAX_MINOR),
    };
    let rule = find_rule(jurisdiction, on_date);
    let weeks = if annual < rule.annual_rent_threshold_minor { rule.weeks_below } else { rule.weeks_at_or_above };
    DepositCap {
        annual_rent: money(annual, "GBP"),
        weekly_rent: money(annual / 52, "GBP"),
        weeks,
        maximum: money(annual * weeks / 52, "GBP"),
    }
}

pub fn deposit_cap_to_value(cap: &DepositCap) -> Value {
    Value::obj(vec![
        ("annualRent", money_to_value(&cap.annual_rent)),
        ("weeklyRent", money_to_value(&cap.weekly_rent)),
        ("weeks", Value::Int(cap.weeks)),
        ("maximum", money_to_value(&cap.maximum)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    deposit_cap_to_value(&deposit_cap(
        &money_from_value(&args[0]),
        args[1].as_str(),
        args[2].as_str(),
        args[3].as_str(),
    ))
}