use super::super::{sanitize, Verdict};
use anyhow::{anyhow, Result};
const LEGAL_WEIGHTS: [u32; 9] = [2, 4, 10, 3, 5, 9, 4, 6, 8];
const INDIVIDUAL_C11_WEIGHTS: [u32; 10] = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8];
const INDIVIDUAL_C12_WEIGHTS: [u32; 11] = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8];
fn weighted_check(digits: &[u32], weights: &[u32]) -> u32 {
let sum: u32 = digits.iter().zip(weights.iter()).map(|(d, w)| d * w).sum();
(sum % 11) % 10
}
fn verify_ru_legal_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<u32> = clean.chars().map(|c| c.to_digit(10).unwrap()).collect();
let expected = weighted_check(&digits[..9], &LEGAL_WEIGHTS);
let got = digits[9];
if expected == got {
Verdict::Valid {
formatted: format!("RU{}", clean),
detected: "Russian INN (legal)".into(),
comment: String::new(),
}
} else {
Verdict::Invalid {
reason: format!("RU INN (legal) check mismatch: expected {}, got {}", expected, got),
}
}
}
pub fn verify_ru_legal(input: &str) -> Verdict {
let clean = match super::strip_vat_prefix(input, "RU") {
Ok(body) => body,
Err(v) => return v,
};
if clean.len() != 10 {
return Verdict::Invalid {
reason: format!("RU INN (legal): expected 10 digits, got {}", clean.len()),
};
}
verify_ru_legal_body(&clean)
}
pub fn create_ru_legal(input: &str, _raw: bool) -> Result<String> {
let clean = sanitize(input, false);
if clean.len() != 9 {
return Err(anyhow!("RU INN (legal): expected 9 digits (body), got {}", clean.len()));
}
if !clean.chars().all(|c| c.is_ascii_digit()) {
return Err(anyhow!("non-digit input"));
}
let digits: Vec<u32> = clean.chars().map(|c| c.to_digit(10).unwrap()).collect();
let check = weighted_check(&digits, &LEGAL_WEIGHTS);
Ok(format!("RU{}{}", clean, check))
}
fn verify_ru_individual_body(clean: &str) -> Verdict {
debug_assert_eq!(clean.len(), 12);
if !clean.chars().all(|c| c.is_ascii_digit()) {
return Verdict::Invalid { reason: "non-digit input".into() };
}
let digits: Vec<u32> = clean.chars().map(|c| c.to_digit(10).unwrap()).collect();
let expected_c11 = weighted_check(&digits[..10], &INDIVIDUAL_C11_WEIGHTS);
let got_c11 = digits[10];
if expected_c11 != got_c11 {
return Verdict::Invalid {
reason: format!(
"RU INN (individual) c11 mismatch: expected {}, got {}",
expected_c11, got_c11
),
};
}
let expected_c12 = weighted_check(&digits[..11], &INDIVIDUAL_C12_WEIGHTS);
let got_c12 = digits[11];
if expected_c12 == got_c12 {
Verdict::Valid {
formatted: format!("RU{}", clean),
detected: "Russian INN (individual)".into(),
comment: String::new(),
}
} else {
Verdict::Invalid {
reason: format!(
"RU INN (individual) c12 mismatch: expected {}, got {}",
expected_c12, got_c12
),
}
}
}
pub fn verify_ru_individual(input: &str) -> Verdict {
let clean = match super::strip_vat_prefix(input, "RU") {
Ok(body) => body,
Err(v) => return v,
};
if clean.len() != 12 {
return Verdict::Invalid {
reason: format!("RU INN (individual): expected 12 digits, got {}", clean.len()),
};
}
verify_ru_individual_body(&clean)
}
pub fn create_ru_individual(input: &str, _raw: bool) -> Result<String> {
let clean = sanitize(input, false);
if clean.len() != 10 {
return Err(anyhow!(
"RU INN (individual): expected 10 digits (body without check digits), got {}",
clean.len()
));
}
if !clean.chars().all(|c| c.is_ascii_digit()) {
return Err(anyhow!("non-digit input"));
}
let digits: Vec<u32> = clean.chars().map(|c| c.to_digit(10).unwrap()).collect();
let c11 = weighted_check(&digits, &INDIVIDUAL_C11_WEIGHTS);
let mut digits11 = digits.clone();
digits11.push(c11);
let c12 = weighted_check(&digits11, &INDIVIDUAL_C12_WEIGHTS);
Ok(format!("RU{}{}{}", clean, c11, c12))
}
pub fn verify_ru_vat(input: &str) -> Verdict {
let clean = match super::strip_vat_prefix(input, "RU") {
Ok(body) => body,
Err(v) => return v,
};
match clean.len() {
10 => verify_ru_legal_body(&clean),
12 => verify_ru_individual_body(&clean),
n => Verdict::Invalid {
reason: format!(
"RU INN: expected 10 (legal) or 12 (individual) digits, got {}",
n
),
},
}
}
pub fn create_ru_vat(_input: &str, _raw: bool) -> Result<String> {
Err(anyhow!(
"ru-vat auto-detect cannot create; use ru-legal or ru-individual to specify the variant"
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ru_legal_valid_7830002293() {
match verify_ru_legal("7830002293") {
Verdict::Valid { formatted, detected, .. } => {
assert_eq!(formatted, "RU7830002293");
assert_eq!(detected, "Russian INN (legal)");
}
v => panic!("{:?}", v),
}
}
#[test]
fn ru_individual_valid_500100732259() {
match verify_ru_individual("500100732259") {
Verdict::Valid { formatted, detected, .. } => {
assert_eq!(formatted, "RU500100732259");
assert_eq!(detected, "Russian INN (individual)");
}
v => panic!("{:?}", v),
}
}
#[test]
fn ru_vat_autodetects_legal() {
match verify_ru_vat("7830002293") {
Verdict::Valid { detected, .. } => assert_eq!(detected, "Russian INN (legal)"),
v => panic!("{:?}", v),
}
}
#[test]
fn ru_vat_autodetects_individual() {
match verify_ru_vat("500100732259") {
Verdict::Valid { detected, .. } => assert_eq!(detected, "Russian INN (individual)"),
v => panic!("{:?}", v),
}
}
#[test]
fn ru_vat_rejects_wrong_length() {
match verify_ru_vat("12345678901") {
Verdict::Invalid { reason } => assert!(reason.contains("expected 10 (legal) or 12")),
v => panic!("{:?}", v),
}
}
#[test]
fn ru_legal_round_trip() {
let body = "783000229";
let full = create_ru_legal(body, false).unwrap();
assert_eq!(full, "RU7830002293");
match verify_ru_legal(&full) {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
#[test]
fn ru_individual_round_trip() {
let body = "5001007322";
let full = create_ru_individual(body, false).unwrap();
assert_eq!(full, "RU500100732259");
match verify_ru_individual(&full) {
Verdict::Valid { .. } => {}
v => panic!("{:?}", v),
}
}
}