use super::funejson::Value; use super::charity_gift_aid_data::{BASIC_RATE, BASIC_RATE_HISTORY}; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; fn is_iso_date(s: &str) -> bool { let b = s.as_bytes(); b.len() == 10 && b.iter().enumerate().all(|(i, c)| if i == 4 || i == 7 { *c == b'-' } else { c.is_ascii_digit() }) } fn basic_rate(on_date: &str) -> i64 { for row in BASIC_RATE { if row.valid_from <= on_date && row.valid_to.map_or(true, |to| on_date <= to) { return row.basis_points; } } // A history=current build carries only the rate in force; answering an old // gift with today's rate would be a quiet wrong claim, so say why instead. if BASIC_RATE_HISTORY != "full" { if let Some(earliest) = BASIC_RATE.iter().map(|r| r.valid_from).min() { if on_date < earliest { panic!( "no Gift Aid basic rate on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier tax years.", on_date, BASIC_RATE_HISTORY, earliest ); } } } panic!("no Gift Aid basic rate on {}", on_date); } /// The tax a charity reclaims on a Gift Aid donation. The donor is treated as /// having given the gift after deducting basic rate tax (ITA 2007 s414), so the /// charity reclaims donation x rate / (100% - rate): 25p per £1 at 20%. It is /// always the UK basic rate, for Scottish and Welsh taxpayers too. /// /// # Panics /// Panics on a non-GBP or negative donation, a malformed date, or a date with /// no basic rate on file. pub fn gift_aid(donation: &Money, on_date: &str) -> GiftAid { if donation.currency != "GBP" { panic!("Gift Aid is claimed in GBP, received {}", donation.currency); } if donation.minor < 0 { panic!("donation must not be negative, received {}", donation.minor); } if !is_iso_date(on_date) { panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date); } let rate = basic_rate(on_date); // Rounded down: a claim may never exceed the tax treated as deducted. let tax = round_div(donation.minor * rate, 10000 - rate, "down"); GiftAid { donation: money(donation.minor, "GBP"), tax_reclaimable: money(tax, "GBP"), gross: money(donation.minor + tax, "GBP"), basic_rate_basis_points: rate, } } pub fn gift_aid_to_value(g: &GiftAid) -> Value { Value::obj(vec![ ("donation", money_to_value(&g.donation)), ("taxReclaimable", money_to_value(&g.tax_reclaimable)), ("gross", money_to_value(&g.gross)), ("basicRateBasisPoints", Value::Int(g.basic_rate_basis_points)), ]) } pub fn fune_vector(args: &[Value]) -> Value { if let Value::Float(f) = args[0].get("minor") { if f.fract() != 0.0 { panic!("money() takes integer minor units, received {}", f); } } gift_aid_to_value(&gift_aid(&money_from_value(&args[0]), args[1].as_str())) }