Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::construction_vat_reverse_charge_data::{
ReverseChargeRule, REVERSE_CHARGE_RULES, REVERSE_CHARGE_RULES_HISTORY, REVERSE_CHARGE_RULES_HORIZON,
};
fn is_iso_date(value: &str) -> bool {
let bytes = value.as_bytes();
bytes.len() == 10
&& bytes[4] == b'-'
&& bytes[7] == b'-'
&& bytes
.iter()
.enumerate()
.all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
}
fn not_applied(reason: &str) -> ReverseChargeDecision {
ReverseChargeDecision {
applies: false,
reason: reason.to_string(),
invoice_wording: None,
}
}
/// Whether the UK VAT domestic reverse charge for building and construction
/// services (VATA 1994 s.55A) applies to one supply.
///
/// The conditions are checked in a fixed order and the first that fails is the
/// reason, so the answer is the same in every language and explains itself.
///
/// # Panics
/// Panics on a malformed date, a share outside 0..=10000 or an unknown category.
pub fn domestic_reverse_charge(supply: &ReverseChargeSupply) -> ReverseChargeDecision {
if !is_iso_date(&supply.supply_date) {
panic!(
"supplyDate must be an ISO date (YYYY-MM-DD), received \"{}\"",
supply.supply_date
);
}
let share = supply.reverse_charge_share_basis_points;
if !(0..=10000).contains(&share) {
panic!(
"reverseChargeShareBasisPoints must be between 0 and 10000, received {}",
share
);
}
let category = supply.vat_category.as_str();
if !["standard", "reduced", "zero", "exempt"].contains(&category) {
panic!(
"unknown vatCategory \"{}\": expected standard, reduced, zero or exempt",
category
);
}
let date = supply.supply_date.as_str();
let mut rule: Option<&ReverseChargeRule> = None;
for row in REVERSE_CHARGE_RULES.iter() {
if row.jurisdiction != "GB" || date < row.valid_from {
continue;
}
if let Some(valid_to) = row.valid_to {
if date > valid_to {
continue;
}
}
if rule.map_or(true, |r| row.valid_from > r.valid_from) {
rule = Some(row);
}
}
let rule = match rule {
Some(rule) => rule,
None => {
// A pruned build must not answer "before the reverse charge started"
// for a date it simply no longer carries rules for.
if let Some(horizon) = REVERSE_CHARGE_RULES_HORIZON {
if date < horizon {
panic!(
"no reverse charge rule for {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full.",
date, REVERSE_CHARGE_RULES_HISTORY, horizon
);
}
}
return not_applied("before-start");
}
};
if !supply.supplier_vat_registered {
return not_applied("supplier-not-vat-registered");
}
if !supply.customer_vat_registered {
return not_applied("customer-not-vat-registered");
}
if !supply.reported_under_cis {
return not_applied("not-reported-under-cis");
}
if category != "standard" && category != "reduced" {
return not_applied("not-standard-or-reduced");
}
if supply.employment_business {
return not_applied("employment-business");
}
if supply.end_user_notified {
return not_applied("end-user-notified");
}
if share <= rule.disregard_basis_points {
return not_applied("five-percent-disregard");
}
ReverseChargeDecision {
applies: true,
reason: "reverse-charge".to_string(),
invoice_wording: Some(rule.invoice_wording.to_string()),
}
}
pub fn reverse_charge_supply_from_value(v: &Value) -> ReverseChargeSupply {
let share = v.get("reverseChargeShareBasisPoints");
if let Value::Float(f) = share {
panic!("reverseChargeShareBasisPoints must be an integer, received {}", f);
}
let date = v.get("supplyDate");
if !matches!(date, Value::Str(_)) {
panic!("supplyDate must be an ISO date (YYYY-MM-DD), received \"{:?}\"", date);
}
ReverseChargeSupply {
supply_date: date.as_str().to_string(),
supplier_vat_registered: v.get("supplierVatRegistered").as_bool(),
customer_vat_registered: v.get("customerVatRegistered").as_bool(),
reported_under_cis: v.get("reportedUnderCis").as_bool(),
vat_category: v.get("vatCategory").as_str().to_string(),
employment_business: v.get("employmentBusiness").as_bool(),
end_user_notified: v.get("endUserNotified").as_bool(),
reverse_charge_share_basis_points: share.as_i64(),
}
}
pub fn reverse_charge_decision_to_value(decision: &ReverseChargeDecision) -> Value {
Value::obj(vec![
("applies", Value::Bool(decision.applies)),
("reason", Value::str(&decision.reason)),
(
"invoiceWording",
match &decision.invoice_wording {
Some(text) => Value::str(text),
None => Value::Null,
},
),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
reverse_charge_decision_to_value(&domestic_reverse_charge(&reverse_charge_supply_from_value(&args[0])))
}