use super::funejson::Value; use super::dates_days_between::days_between; use super::energy_meter_advance::meter_advance; use super::math_round_div::round_div; use super::money_amount::{money, money_to_value}; const MAX_SAFE: i128 = 9_007_199_254_740_991; fn check_rate(what: &str, value: i64) { if value < 0 { panic!("{} must be a whole number of 0 or more, received {}", what, value); } } fn exact(product: i128) -> i64 { if product > MAX_SAFE { panic!("water charge too large to calculate exactly"); } product as i64 } /// A metered water and sewerage bill: volumetric water, sewerage on the share /// returned to the sewer, and standing and surface water charges apportioned /// by days / 365, each line rounded once. /// /// # Panics /// Panics on reads that do not fit the register, reads in the wrong order, a /// negative charge, a return-to-sewer share outside 0-100%, or a charge too /// large to be exact. pub fn water_bill( opening_read: &MeterRead, closing_read: &MeterRead, meter_digits: i64, tariff: &WaterTariff, surface_water_drains: bool, currency: &str, mode: &str, ) -> WaterBill { let cubic_metres = meter_advance(opening_read.value, closing_read.value, meter_digits); let days = days_between(&opening_read.date, &closing_read.date); if days < 0 { panic!( "the closing read must not be dated before the opening read, received {} and {}", opening_read.date, closing_read.date ); } check_rate("waterVolumeRate", tariff.water_volume_rate); check_rate("sewerageVolumeRate", tariff.sewerage_volume_rate); check_rate("waterStandingCharge", tariff.water_standing_charge); check_rate("sewerageStandingCharge", tariff.sewerage_standing_charge); check_rate("surfaceWaterCharge", tariff.surface_water_charge); if tariff.return_to_sewer < 0 || tariff.return_to_sewer > 10000 { panic!("returnToSewer must be basis points from 0 to 10000, received {}", tariff.return_to_sewer); } let m3 = cubic_metres as i128; let d = days as i128; let water = round_div(exact(m3 * tariff.water_volume_rate as i128), 1000, mode); let sewerage = round_div( exact(m3 * tariff.sewerage_volume_rate as i128 * tariff.return_to_sewer as i128), 10_000_000, mode, ); let water_standing = round_div(exact(tariff.water_standing_charge as i128 * d), 365, mode); let sewerage_standing = round_div(exact(tariff.sewerage_standing_charge as i128 * d), 365, mode); let surface = if surface_water_drains { round_div(exact(tariff.surface_water_charge as i128 * d), 365, mode) } else { 0 }; WaterBill { cubic_metres, days, water_volume_charge: money(water, currency), sewerage_volume_charge: money(sewerage, currency), water_standing_charge: money(water_standing, currency), sewerage_standing_charge: money(sewerage_standing, currency), surface_water_charge: money(surface, currency), total: money(water + sewerage + water_standing + sewerage_standing + surface, currency), } } fn whole(v: &Value, what: &str) -> i64 { if let Value::Float(f) = v { panic!("{} must be a whole number of 0 or more, received {}", what, f); } v.as_i64() } pub fn meter_read_from_value(v: &Value) -> MeterRead { if let Value::Float(f) = v.get("value") { panic!("reads must be whole numbers, received {}", f); } MeterRead { date: v.get("date").as_str().to_string(), value: v.get("value").as_i64(), } } pub fn water_tariff_from_value(v: &Value) -> WaterTariff { WaterTariff { water_volume_rate: whole(v.get("waterVolumeRate"), "waterVolumeRate"), sewerage_volume_rate: whole(v.get("sewerageVolumeRate"), "sewerageVolumeRate"), water_standing_charge: whole(v.get("waterStandingCharge"), "waterStandingCharge"), sewerage_standing_charge: whole(v.get("sewerageStandingCharge"), "sewerageStandingCharge"), surface_water_charge: whole(v.get("surfaceWaterCharge"), "surfaceWaterCharge"), return_to_sewer: whole(v.get("returnToSewer"), "returnToSewer"), } } pub fn water_bill_to_value(b: &WaterBill) -> Value { Value::obj(vec![ ("cubicMetres", Value::Int(b.cubic_metres)), ("days", Value::Int(b.days)), ("waterVolumeCharge", money_to_value(&b.water_volume_charge)), ("sewerageVolumeCharge", money_to_value(&b.sewerage_volume_charge)), ("waterStandingCharge", money_to_value(&b.water_standing_charge)), ("sewerageStandingCharge", money_to_value(&b.sewerage_standing_charge)), ("surfaceWaterCharge", money_to_value(&b.surface_water_charge)), ("total", money_to_value(&b.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { water_bill_to_value(&water_bill( &meter_read_from_value(&args[0]), &meter_read_from_value(&args[1]), args[2].as_i64(), &water_tariff_from_value(&args[3]), args[4].as_bool(), args[5].as_str(), args[6].as_str(), )) }