use super::funejson::Value; use super::math_round_div::round_div; use super::money_amount::{money_from_value, Money}; /// Above this, loan × 10000 is no longer exact as a JavaScript number. const MAX_LOAN: i64 = 900_719_925_474; /// Loan-to-value, and the band from the lender's table. The band is chosen on /// the exact ratio, loan × 10000 <= ceiling × value, never on a rounded /// percentage, so 75.004% is not "75%". The reported basis points are rounded /// up, which agrees: a whole ceiling is met exactly when the rounded-up ratio /// is within it. /// /// # Panics /// Panics on mismatched currencies, a value of zero or less, a negative or /// oversized loan, or an empty or unordered band table. pub fn loan_to_value(loan: &Money, property_value: &Money, bands: &[LtvBand]) -> LtvResult { if loan.currency != property_value.currency { panic!("currency mismatch: {} and {}", loan.currency, property_value.currency); } if property_value.minor <= 0 { panic!("propertyValue must be greater than zero, received {}", property_value.minor); } if loan.minor < 0 { panic!("loan must not be negative, received {}", loan.minor); } if loan.minor > MAX_LOAN { panic!("loan must be at most {} minor units", MAX_LOAN); } if bands.is_empty() { panic!("bands must not be empty"); } for i in 0..bands.len() { let ceiling = bands[i].max_basis_points; if ceiling < 1 { panic!("band ceilings must be whole basis points of 1 or more, received {}", ceiling); } if i > 0 && ceiling <= bands[i - 1].max_basis_points { panic!("band ceilings must be in strictly ascending order"); } } let basis_points = round_div(loan.minor * 10000, property_value.minor, "up"); let band = bands .iter() .find(|band| basis_points <= band.max_basis_points) .map(|band| band.label.clone()); LtvResult { basis_points, band } } pub fn ltv_band_from_value(v: &Value) -> LtvBand { LtvBand { max_basis_points: v.get("maxBasisPoints").as_i64(), label: v.get("label").as_str().to_string(), } } pub fn ltv_result_to_value(result: &LtvResult) -> Value { Value::obj(vec![ ("basisPoints", Value::Int(result.basis_points)), ( "band", match &result.band { Some(label) => Value::str(label), None => Value::Null, }, ), ]) } pub fn fune_vector(args: &[Value]) -> Value { let bands: Vec = args[2].as_arr().iter().map(ltv_band_from_value).collect(); ltv_result_to_value(&loan_to_value( &money_from_value(&args[0]), &money_from_value(&args[1]), &bands, )) }