use super::funejson::Value; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::property_deposit_cap_data::{DepositCapRule, DEPOSIT_CAPS, DEPOSIT_CAPS_HISTORY}; // 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(), )) }