use super::funejson::Value; use super::charity_gift_aid_eligibility_data::{BenefitLimit, BENEFIT_LIMITS, BENEFIT_LIMITS_HISTORY}; 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 limits_on(on_date: &str) -> &'static BenefitLimit { for row in BENEFIT_LIMITS { if row.valid_from <= on_date && row.valid_to.map_or(true, |to| on_date <= to) { return row; } } if BENEFIT_LIMITS_HISTORY != "full" { if let Some(earliest) = BENEFIT_LIMITS.iter().map(|r| r.valid_from).min() { if on_date < earliest { panic!( "no Gift Aid benefit limits on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier tax years.", on_date, BENEFIT_LIMITS_HISTORY, earliest ); } } } panic!("no Gift Aid benefit limits on {}", on_date); } fn check_gbp(name: &str, amount: &Money) { if amount.currency != "GBP" { panic!("Gift Aid amounts must be in GBP, received {} for {}", amount.currency, name); } } /// Whether a donation qualifies for Gift Aid (ITA 2007 ss416-418). The benefit /// test is done on the exact limit, scaled by 10000, so 25% or 5% of an odd /// number of pence is never rounded before the comparison. /// /// # Panics /// Panics on a non-GBP amount, a donation that is not positive, a negative /// benefit, a malformed date, or a date with no limits on file. pub fn gift_aid_eligibility( donation: &Money, benefit_value: &Money, prior_benefits_this_year: &Money, has_declaration: bool, donor_pays_enough_tax: bool, on_date: &str, ) -> GiftAidEligibility { check_gbp("donation", donation); check_gbp("benefitValue", benefit_value); check_gbp("priorBenefitsThisYear", prior_benefits_this_year); if donation.minor <= 0 { panic!("donation must be positive, received {}", donation.minor); } if benefit_value.minor < 0 { panic!("benefitValue must not be negative, received {}", benefit_value.minor); } if prior_benefits_this_year.minor < 0 { panic!("priorBenefitsThisYear must not be negative, received {}", prior_benefits_this_year.minor); } if !is_iso_date(on_date) { panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date); } let rule = limits_on(on_date); // The variable limit, in pence x 10000. let scaled_limit: i64 = if donation.minor <= rule.small_gift_up_to { donation.minor * rule.small_gift_basis_points } else { match (rule.middle_gift_up_to, rule.middle_gift_fixed) { (Some(up_to), Some(fixed)) if donation.minor <= up_to => fixed * 10000, _ => rule.excess_fixed * 10000 + (donation.minor - rule.excess_from) * rule.excess_basis_points, } }; let mut reasons: Vec = Vec::new(); if !has_declaration { reasons.push("no-declaration".to_string()); } if !donor_pays_enough_tax { reasons.push("insufficient-tax".to_string()); } // "exceeds" and "is more than": a benefit exactly at a limit is allowed. if benefit_value.minor * 10000 > scaled_limit { reasons.push("benefit-over-limit".to_string()); } if benefit_value.minor + prior_benefits_this_year.minor > rule.annual_limit { reasons.push("annual-benefits-over-limit".to_string()); } GiftAidEligibility { eligible: reasons.is_empty(), reasons, benefit_limit: money(scaled_limit.div_euclid(10000), "GBP"), annual_benefit_limit: money(rule.annual_limit, "GBP"), } } pub fn gift_aid_eligibility_to_value(e: &GiftAidEligibility) -> Value { Value::obj(vec![ ("eligible", Value::Bool(e.eligible)), ("reasons", Value::Arr(e.reasons.iter().map(|r| Value::str(r)).collect())), ("benefitLimit", money_to_value(&e.benefit_limit)), ("annualBenefitLimit", money_to_value(&e.annual_benefit_limit)), ]) } pub fn fune_vector(args: &[Value]) -> Value { gift_aid_eligibility_to_value(&gift_aid_eligibility( &money_from_value(&args[0]), &money_from_value(&args[1]), &money_from_value(&args[2]), args[3].as_bool(), args[4].as_bool(), args[5].as_str(), )) }