use super::super::{sanitize, Verdict};
use anyhow::{anyhow, Result};
const WEIGHTS: [u32; 8] = [29, 23, 19, 17, 13, 7, 5, 3];
const ALPHABET: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const ALPHA_SECOND: &str = "ABCEHKMOPT";
const VALID_FIRST: &str = "1234567ABCEHKM";
fn alpha_index(c: char) -> Option<u32> {
ALPHABET.chars().position(|x| x == c).map(|i| i as u32)
}
fn compute_check(body: &str) -> Option<u32> {
debug_assert_eq!(body.len(), 8);
let converted: String = if !body.chars().all(|c| c.is_ascii_digit()) {
let first = body.chars().next().unwrap();
let second = body.chars().nth(1).unwrap();
let second_sub = if let Some(idx) = ALPHA_SECOND.chars().position(|x| x == second) {
std::char::from_digit(idx as u32, 10).unwrap()
} else {
second
};
let rest: String = body.chars().skip(2).collect();
format!("{}{}{}", first, second_sub, rest)
} else {
body.to_string()
};
let sum: u32 = converted
.chars()
.zip(WEIGHTS.iter())
.map(|(c, &w)| w * alpha_index(c).unwrap_or(0))
.sum();
let r = sum % 11;
if r > 9 { None } else { Some(r) }
}
fn is_valid_format(s: &str) -> bool {
if s.len() != 9 {
return false;
}
let chars: Vec<char> = s.chars().collect();
if !chars[2..].iter().all(|c| c.is_ascii_digit()) {
return false;
}
let first_two_alpha = chars[0].is_ascii_alphabetic() || chars[1].is_ascii_alphabetic();
if first_two_alpha {
if !chars[0].is_ascii_alphabetic() || !chars[1].is_ascii_alphabetic() {
return false; }
if !ALPHA_SECOND.contains(chars[0]) || !ALPHA_SECOND.contains(chars[1]) {
return false;
}
}
if !VALID_FIRST.contains(chars[0]) {
return false;
}
true
}
pub fn verify_by_vat(input: &str) -> Verdict {
let raw = sanitize(input, true);
let clean = match super::strip_vat_prefix(&raw, "BY") {
Ok(body) => body,
Err(v) => return v,
};
if clean.len() != 9 {
return Verdict::Invalid {
reason: format!("BY UNP: expected 9 characters, got {}", clean.len()),
};
}
if !is_valid_format(&clean) {
return Verdict::Invalid {
reason: "BY UNP: invalid format (positions 3-9 must be digits; first two must be \
numeric or from A,B,C,E,H,K,M,O,P,T)".into(),
};
}
let body = &clean[..8];
let stored: u32 = clean.chars().nth(8).unwrap().to_digit(10).unwrap();
match compute_check(body) {
None => Verdict::Invalid {
reason: "BY UNP: check digit computation yields 10 (structurally invalid number)".into(),
},
Some(expected) if expected == stored => Verdict::Valid {
formatted: format!("BY{}", clean),
detected: "Belarusian UNP".into(),
comment: String::new(),
},
Some(expected) => Verdict::Invalid {
reason: format!("BY UNP check mismatch: expected {}, got {}", expected, stored),
},
}
}
pub fn create_by_vat(input: &str, _raw: bool) -> Result<String> {
let clean = sanitize(input, true);
if clean.len() != 8 {
return Err(anyhow!(
"BY UNP: expected 8 characters (body without check digit), got {}",
clean.len()
));
}
match compute_check(&clean) {
None => Err(anyhow!("BY UNP: check digit computation yields 10 (structurally invalid body)")),
Some(check) => Ok(format!("BY{}{}", clean, check)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn by_vat_valid_200988541() {
match verify_by_vat("200988541") {
Verdict::Valid { formatted, detected, .. } => {
assert_eq!(formatted, "BY200988541");
assert_eq!(detected, "Belarusian UNP");
}
v => panic!("{:?}", v),
}
}
#[test]
fn by_vat_valid_ma1953684() {
match verify_by_vat("MA1953684") {
Verdict::Valid { formatted, detected, .. } => {
assert_eq!(formatted, "BYMA1953684");
assert_eq!(detected, "Belarusian UNP");
}
v => panic!("{:?}", v),
}
}
#[test]
fn by_vat_rejects_wrong_length() {
match verify_by_vat("20098854") {
Verdict::Invalid { reason } => assert!(reason.contains("expected 9 characters")),
v => panic!("{:?}", v),
}
}
#[test]
fn by_vat_round_trip_numeric() {
let body = "20098854";
let full = create_by_vat(body, false).unwrap();
assert_eq!(full, "BY200988541");
match verify_by_vat(&full) {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn by_vat_round_trip_alpha() {
let body = "MA195368";
let full = create_by_vat(body, false).unwrap();
assert_eq!(full, "BYMA1953684");
match verify_by_vat(&full) {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn by_vat_rejects_bad_check() {
match verify_by_vat("200988542") {
Verdict::Invalid { .. } => {}
v => panic!("{:?}", v),
}
}
}