use super::funejson::Value; use super::finance_markup_price::markup_price; use super::finance_tax_add_vat::add_vat; use super::math_round_div::round_div; use super::money_add::add_money; use super::money_amount::{money, money_from_value, money_to_value, Money}; const MAX_SAFE: i128 = 9_007_199_254_740_991; /// A quote from labour days, a day rate, marked-up materials and VAT. /// /// The markup goes on the materials only: the day rate already is the price of /// labour. Labour is rounded once, to the penny, from days x rate; VAT is one /// rounding on the net total, as on the invoice that follows. /// /// # Panics /// Panics on negative days or rates, mixed currencies, an unknown VAT /// category, or a product beyond 2^53 - 1. pub fn day_rate_quote( labour_days_hundredths: i64, day_rate: &Money, materials_cost: &Money, materials_markup_basis_points: i64, vat_category: Option<&str>, on_date: &str, ) -> DayRateQuote { if labour_days_hundredths < 0 { panic!( "labourDaysHundredths must be a whole number of hundredths, not negative, received {}", labour_days_hundredths ); } if day_rate.minor < 0 { panic!("dayRate must not be negative, received {}", day_rate.minor); } let product = day_rate.minor as i128 * labour_days_hundredths as i128; // i64 could go further, but TypeScript cannot, and all three must agree. if product > MAX_SAFE { panic!("the labour calculation exceeds 2^53 - 1"); } let labour = money(round_div(product as i64, 100, "half-up"), &day_rate.currency); let materials = markup_price(materials_cost, materials_markup_basis_points, "markup", "half-up"); let net = add_money(&labour, &materials); match vat_category { None => DayRateQuote { vat: money(0, &net.currency), gross: net.clone(), labour, materials, net, vat_basis_points: 0, }, Some(category) => { let vat = add_vat(&net, "GB", category, on_date); DayRateQuote { labour, materials, net, vat: vat.tax, gross: vat.gross, vat_basis_points: vat.basis_points, } } } } pub fn day_rate_quote_to_value(q: &DayRateQuote) -> Value { Value::obj(vec![ ("labour", money_to_value(&q.labour)), ("materials", money_to_value(&q.materials)), ("net", money_to_value(&q.net)), ("vat", money_to_value(&q.vat)), ("gross", money_to_value(&q.gross)), ("vatBasisPoints", Value::Int(q.vat_basis_points)), ]) } pub fn fune_vector(args: &[Value]) -> Value { if let Value::Float(f) = &args[0] { panic!( "labourDaysHundredths must be a whole number of hundredths, not negative, received {}", f ); } if let Value::Float(f) = &args[3] { panic!("basisPoints must be an integer, received {}", f); } let category = if args[4].is_null() { None } else { Some(args[4].as_str()) }; day_rate_quote_to_value(&day_rate_quote( args[0].as_i64(), &money_from_value(&args[1]), &money_from_value(&args[2]), args[3].as_i64(), category, args[5].as_str(), )) }