use super::super::{sanitize, Verdict};
use anyhow::{anyhow, Result};
const CHECK_TABLE: &[u8] = b"WABCDEFGHIJKLMNOPQRSTUV";
const WEIGHTS_7: [u32; 7] = [8, 7, 6, 5, 4, 3, 2];
fn sum_7digits(body: &str) -> u32 {
body.chars()
.enumerate()
.map(|(i, c)| WEIGHTS_7[i] * c.to_digit(10).unwrap())
.sum()
}
fn check_letter(idx: u32) -> char {
CHECK_TABLE[(idx % 23) as usize] as char
}
fn suffix_value(c: char) -> Option<u32> {
let uc = c.to_ascii_uppercase();
if ('A'..='I').contains(&uc) {
Some((uc as u32) - ('A' as u32) + 1)
} else {
None
}
}
pub fn verify_ie_vat(input: &str) -> Verdict {
let clean = match super::strip_vat_prefix(input, "IE") {
Ok(body) => body,
Err(v) => return v,
};
let len = clean.len();
if clean.starts_with('0') && len == 8 {
return Verdict::Invalid {
reason: "IE VAT Format 3 (starts with '0', historical) is not supported".into(),
};
}
match len {
8 => {
let body = &clean[..7];
let check_char = clean.chars().nth(7).unwrap();
if !body.chars().all(|c| c.is_ascii_digit()) {
return Verdict::Invalid {
reason: "IE VAT Format 1: first 7 characters must be digits".into(),
};
}
if !check_char.is_ascii_alphabetic() {
return Verdict::Invalid {
reason: "IE VAT Format 1: 8th character must be a letter".into(),
};
}
let sum = sum_7digits(body);
let expected = check_letter(sum % 23);
if expected == check_char {
Verdict::Valid {
formatted: format!("IE{}", clean),
detected: "Irish VAT (Format 1, old)".into(),
comment: String::new(),
}
} else {
Verdict::Invalid {
reason: format!(
"IE VAT check mismatch: expected '{}', got '{}'",
expected, check_char
),
}
}
}
9 => {
let digits_part = &clean[..7];
let suffix_char = clean.chars().nth(7).unwrap();
let check_char = clean.chars().nth(8).unwrap();
if !digits_part.chars().all(|c| c.is_ascii_digit()) {
return Verdict::Invalid {
reason: "IE VAT Format 2: first 7 characters must be digits".into(),
};
}
if !suffix_char.is_ascii_alphabetic() || !check_char.is_ascii_alphabetic() {
return Verdict::Invalid {
reason: "IE VAT Format 2: 8th and 9th characters must be letters".into(),
};
}
let sv = match suffix_value(suffix_char) {
Some(v) => v,
None => {
return Verdict::Invalid {
reason: format!(
"IE VAT Format 2: suffix letter must be A–I, got '{}'",
suffix_char
),
}
}
};
let sum = sum_7digits(digits_part) + sv * 9;
let expected = check_letter(sum % 23);
if expected == check_char {
Verdict::Valid {
formatted: format!("IE{}", clean),
detected: "Irish VAT (Format 2, 2013+)".into(),
comment: String::new(),
}
} else {
Verdict::Invalid {
reason: format!(
"IE VAT check mismatch: expected '{}', got '{}'",
expected, check_char
),
}
}
}
_ => Verdict::Invalid {
reason: format!("expected 8 or 9 characters for IE VAT, got {}", len),
},
}
}
pub fn create_ie_vat(input: &str, _raw: bool) -> Result<String> {
let clean = sanitize(input, true);
let len = clean.len();
match len {
7 => {
if !clean.chars().all(|c| c.is_ascii_digit()) {
return Err(anyhow!("IE VAT Format 1 create: expected 7 digits"));
}
let sum = sum_7digits(&clean);
let letter = check_letter(sum % 23);
Ok(format!("IE{}{}", clean, letter))
}
8 => {
let digits_part = &clean[..7];
let suffix_char = clean.chars().nth(7).unwrap();
if !digits_part.chars().all(|c| c.is_ascii_digit()) {
return Err(anyhow!("IE VAT Format 2 create: first 7 characters must be digits"));
}
let sv = suffix_value(suffix_char).ok_or_else(|| {
anyhow!(
"IE VAT Format 2 create: suffix letter must be A–I, got '{}'",
suffix_char
)
})?;
let sum = sum_7digits(digits_part) + sv * 9;
let letter = check_letter(sum % 23);
Ok(format!("IE{}{}", clean, letter))
}
_ => Err(anyhow!(
"expected 7 digits (Format 1) or 7 digits + suffix letter (Format 2), got {} chars",
len
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ie_vat_format1_valid_1234567t() {
match verify_ie_vat("1234567T") {
Verdict::Valid { detected, .. } => {
assert!(detected.contains("Format 1"), "detected: {}", detected);
}
v => panic!("{:?}", v),
}
}
#[test]
fn ie_vat_format1_round_trip() {
let body = "1234567";
let full = create_ie_vat(body, false).unwrap();
let raw = &full[2..]; match verify_ie_vat(raw) {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn ie_vat_format1_rejects_bad_check() {
match verify_ie_vat("1234567A") {
Verdict::Invalid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn ie_vat_format2_round_trip() {
let created = create_ie_vat("1234567A", false).unwrap();
let raw = &created[2..];
match verify_ie_vat(raw) {
Verdict::Valid { detected, .. } => {
assert!(detected.contains("Format 2"), "detected: {}", detected);
}
v => panic!("{:?}", v),
}
}
#[test]
fn ie_vat_rejects_wrong_length() {
match verify_ie_vat("123456") {
Verdict::Invalid { .. } => {}
v => panic!("{:?}", v),
}
}
}