use super::super::{sanitize, Verdict};
use anyhow::{anyhow, Result};
const EDRPOU_WEIGHTS_STD: [u64; 7] = [1, 2, 3, 4, 5, 6, 7];
const EDRPOU_WEIGHTS_345: [u64; 7] = [7, 1, 2, 3, 4, 5, 6];
fn edrpou_calc_check(digits: &[u64; 8]) -> Option<u64> {
let w = if digits[0] >= 3 && digits[0] <= 5 {
EDRPOU_WEIGHTS_345
} else {
EDRPOU_WEIGHTS_STD
};
let total: u64 = digits[..7].iter().zip(w.iter()).map(|(d, wt)| d * wt).sum();
let r = total % 11;
if r < 10 {
return Some(r);
}
let w2: [u64; 7] = [w[0]+2, w[1]+2, w[2]+2, w[3]+2, w[4]+2, w[5]+2, w[6]+2];
let total2: u64 = digits[..7].iter().zip(w2.iter()).map(|(d, wt)| d * wt).sum();
let r2 = total2 % 11 % 10;
Some(r2)
}
fn verify_ua_legal_body(clean: &str) -> Verdict {
debug_assert_eq!(clean.len(), 8);
if !clean.chars().all(|c| c.is_ascii_digit()) {
return Verdict::Invalid { reason: "non-digit input".into() };
}
let digits: [u64; 8] = {
let v: Vec<u64> = clean.chars().map(|c| c.to_digit(10).unwrap() as u64).collect();
[v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7]]
};
let check = edrpou_calc_check(&digits).unwrap();
if check == digits[7] {
Verdict::Valid {
formatted: format!("UA{}", clean),
detected: "Ukrainian EDRPOU (legal entity, 8 digits)".into(),
comment: String::new(),
}
} else {
Verdict::Invalid {
reason: format!("UA EDRPOU check mismatch: expected {}, got {}", check, digits[7]),
}
}
}
pub fn verify_ua_legal(input: &str) -> Verdict {
let clean = match super::strip_vat_prefix(input, "UA") {
Ok(body) => body,
Err(v) => return v,
};
if clean.len() != 8 {
return Verdict::Invalid {
reason: format!("UA EDRPOU: expected 8 digits, got {}", clean.len()),
};
}
verify_ua_legal_body(&clean)
}
pub fn create_ua_legal(input: &str, _raw: bool) -> Result<String> {
let clean = sanitize(input, false);
if clean.len() != 7 {
return Err(anyhow!("expected 7 digits (body without check digit), got {}", clean.len()));
}
if !clean.chars().all(|c| c.is_ascii_digit()) {
return Err(anyhow!("non-digit input"));
}
let v: Vec<u64> = clean.chars().map(|c| c.to_digit(10).unwrap() as u64).collect();
let digits: [u64; 8] = [v[0], v[1], v[2], v[3], v[4], v[5], v[6], 0];
let check = edrpou_calc_check(&digits).unwrap();
Ok(format!("UA{}{}", clean, check))
}
const RNOKPP_WEIGHTS: [i64; 9] = [-1, 5, 7, 9, 4, 6, 10, 5, 7];
fn verify_ua_individual_body(clean: &str) -> Verdict {
debug_assert_eq!(clean.len(), 10);
if !clean.chars().all(|c| c.is_ascii_digit()) {
return Verdict::Invalid { reason: "non-digit input".into() };
}
let digits: Vec<i64> = clean.chars().map(|c| c.to_digit(10).unwrap() as i64).collect();
let sum: i64 = digits[..9].iter().zip(RNOKPP_WEIGHTS.iter()).map(|(d, w)| d * w).sum();
let check = (sum % 11).rem_euclid(11) % 10;
if check == digits[9] {
Verdict::Valid {
formatted: format!("UA{}", clean),
detected: "Ukrainian RNOKPP (individual taxpayer, 10 digits)".into(),
comment: String::new(),
}
} else {
Verdict::Invalid {
reason: format!("UA RNOKPP check mismatch: expected {}, got {}", check, digits[9]),
}
}
}
pub fn verify_ua_individual(input: &str) -> Verdict {
let clean = match super::strip_vat_prefix(input, "UA") {
Ok(body) => body,
Err(v) => return v,
};
if clean.len() != 10 {
return Verdict::Invalid {
reason: format!("UA RNOKPP: expected 10 digits, got {}", clean.len()),
};
}
verify_ua_individual_body(&clean)
}
pub fn create_ua_individual(input: &str, _raw: bool) -> Result<String> {
let clean = sanitize(input, false);
if clean.len() != 9 {
return Err(anyhow!("expected 9 digits (body without check digit), got {}", clean.len()));
}
if !clean.chars().all(|c| c.is_ascii_digit()) {
return Err(anyhow!("non-digit input"));
}
let digits: Vec<i64> = clean.chars().map(|c| c.to_digit(10).unwrap() as i64).collect();
let sum: i64 = digits.iter().zip(RNOKPP_WEIGHTS.iter()).map(|(d, w)| d * w).sum();
let check = (sum % 11).rem_euclid(11) % 10;
Ok(format!("UA{}{}", clean, check))
}
pub fn verify_ua_vat(input: &str) -> Verdict {
let clean = match super::strip_vat_prefix(input, "UA") {
Ok(body) => body,
Err(v) => return v,
};
match clean.len() {
8 => verify_ua_legal_body(&clean),
10 => verify_ua_individual_body(&clean),
n => Verdict::Invalid {
reason: format!("UA VAT: expected 8 (EDRPOU) or 10 (RNOKPP) digits, got {}", n),
},
}
}
pub fn create_ua_vat(input: &str, raw: bool) -> Result<String> {
let clean = sanitize(input, false);
match clean.len() {
7 => create_ua_legal(input, raw),
9 => create_ua_individual(input, raw),
n => Err(anyhow!("UA VAT: expected 7 (EDRPOU body) or 9 (RNOKPP body) digits, got {}", n)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ua_edrpou_reference_32855961() {
match verify_ua_vat("32855961") {
Verdict::Valid { formatted, detected, .. } => {
assert_eq!(formatted, "UA32855961");
assert!(detected.contains("EDRPOU"));
}
v => panic!("{:?}", v),
}
}
#[test]
fn ua_edrpou_rejects_bad_check() {
match verify_ua_vat("32855968") {
Verdict::Invalid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn ua_edrpou_accepts_ua_prefix() {
match verify_ua_vat("UA32855961") {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn ua_edrpou_round_trip() {
let body = "3285596";
let full = create_ua_legal(body, false).unwrap();
assert_eq!(full, "UA32855961");
match verify_ua_legal(&full) {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn ua_rnokpp_reference_1759013776() {
match verify_ua_vat("1759013776") {
Verdict::Valid { formatted, detected, .. } => {
assert_eq!(formatted, "UA1759013776");
assert!(detected.contains("RNOKPP"));
}
v => panic!("{:?}", v),
}
}
#[test]
fn ua_rnokpp_reference_2530414071() {
match verify_ua_vat("2530414071") {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn ua_rnokpp_rejects_bad_check() {
match verify_ua_vat("1759013770") {
Verdict::Invalid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn ua_rnokpp_accepts_ua_prefix() {
match verify_ua_vat("UA1759013776") {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn ua_rnokpp_round_trip() {
let body = "175901377";
let full = create_ua_individual(body, false).unwrap();
assert_eq!(full, "UA1759013776");
match verify_ua_individual(&full) {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn ua_vat_rejects_bad_length() {
match verify_ua_vat("12345") {
Verdict::Invalid { reason } => assert!(reason.contains("8")),
v => panic!("{:?}", v),
}
}
}