From f47fd9247195aef3d0b0c19adc84be2c278a4dc9 Mon Sep 17 00:00:00 2001 From: Mollemoll Date: Fri, 3 May 2024 09:37:03 +0200 Subject: [PATCH] feat: vies lookup --- src/eu_vat.rs | 1 + src/eu_vat/vies.rs | 180 +++++++++++++++++++++++++++++++++++++++++++++ src/tax_id.rs | 4 +- 3 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 src/eu_vat/vies.rs diff --git a/src/eu_vat.rs b/src/eu_vat.rs index 5e3ea62..ef9052f 100644 --- a/src/eu_vat.rs +++ b/src/eu_vat.rs @@ -1,4 +1,5 @@ mod syntax; +mod vies; use lazy_static::lazy_static; use syntax::EU_VAT_PATTERNS; diff --git a/src/eu_vat/vies.rs b/src/eu_vat/vies.rs new file mode 100644 index 0000000..eadd052 --- /dev/null +++ b/src/eu_vat/vies.rs @@ -0,0 +1,180 @@ +use std::collections::HashMap; +use roxmltree; +use crate::verification::{Verifier, Verification, VerificationStatus}; +use crate::tax_id::TaxId; +use crate::errors::VerificationError; + +static URI: &'static str = "http://ec.europa.eu/taxation_customs/vies/services/checkVatService"; +static ENVELOPE: &'static str = " + + + + + {country} + {number} + + + +"; + +pub struct VIES; + +impl VIES { + fn xml_to_hash(xml: &roxmltree::Document) -> HashMap { + let mut hash = HashMap::new(); + + for node in xml.descendants() { + let tag_name = node.tag_name().name(); + if let Some(text) = node.text() { + hash.insert(tag_name.to_string(), text.to_string()); + } + } + + hash + } +} + +impl Verifier for VIES { + async fn make_request(&self, tax_id: TaxId) -> Result { + let client = reqwest::Client::new(); + let body = ENVELOPE + .replace("{country}", tax_id.tax_country_code()) + .replace("{number}", tax_id.local_value()); + let res = client + .post(URI) + .header("Content-Type", "text/xml") + .body(body) + .send() + .await + .map_err(VerificationError::HttpError)? + .text() + .await + .map_err(VerificationError::HttpError)?; + + Ok(res) + } + + fn parse_response(&self, response: String) -> Result { + let doc = roxmltree::Document::parse(&response).map_err(VerificationError::ParsingError)?; + let hash = VIES::xml_to_hash(&doc); + let fault = hash.get("faultcode"); + + if fault.is_some() { + return Ok( + Verification::new( + VerificationStatus::Unavailable + ) + ); + } else { + Ok( + Verification::new( + if hash.get("valid").unwrap() == "true" { + VerificationStatus::Verified + } else if hash.get("valid").unwrap() == "false" { + VerificationStatus::Unverified + } else { + panic!("Unexpected response") + } + ) + ) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_xml_to_hash() { + let xml = r#" + + + + + SE + 123456789101 + 2021-01-01+01:00 + true + Test Company +
Test Address
+
+
+
+ "#; + let doc = roxmltree::Document::parse(xml).unwrap(); + let hash = VIES::xml_to_hash(&doc); + + assert_eq!(hash.get("countryCode"), Some(&"SE".to_string())); + assert_eq!(hash.get("vatNumber"), Some(&"123456789101".to_string())); + assert_eq!(hash.get("requestDate"), Some(&"2021-01-01+01:00".to_string())); + assert_eq!(hash.get("valid"), Some(&"true".to_string())); + assert_eq!(hash.get("name"), Some(&"Test Company".to_string())); + assert_eq!(hash.get("address"), Some(&"Test Address".to_string())); + } + + #[test] + fn test_parse_response_verified() { + let response = r#" + + + + + SE + 123456789101 + 2021-01-01+01:00 + true + Test Company +
Test Address
+
+
+
+ "#; + let verifier = VIES; + let verification = verifier.parse_response(response.to_string()).unwrap(); + + assert_eq!(*verification.status(), VerificationStatus::Verified); + } + + #[test] + fn test_parse_response_unverified() { + let response = r#" + + + + + SE + 123456789101 + 2021-01-01+01:00 + false + Test Company +
Test Address
+
+
+
+ "#; + let verifier = VIES; + let verification = verifier.parse_response(response.to_string()).unwrap(); + + assert_eq!(*verification.status(), VerificationStatus::Unverified); + } + + #[test] + fn test_parse_response_unavailable() { + let response = r#" + + + + + env:Server + MS_MAX_CONCURRENT_REQ + + + + "#; + let verifier = VIES; + let verification = verifier.parse_response(response.to_string()).unwrap(); + + assert_eq!(*verification.status(), VerificationStatus::Unavailable); + } +} diff --git a/src/tax_id.rs b/src/tax_id.rs index e144cec..03d4836 100644 --- a/src/tax_id.rs +++ b/src/tax_id.rs @@ -1,4 +1,3 @@ -use std::process::id; use crate::eu_vat::EUVat; use crate::gb_vat::GBVat; use crate::errors::ValidationError; @@ -10,7 +9,7 @@ pub trait TaxIdType { fn country_code_from(&self, tax_country_code: &str) -> String; } -struct TaxId { +pub struct TaxId { value: String, country_code: String, tax_country_code: String, @@ -43,6 +42,7 @@ impl TaxId { pub fn value(&self) -> &str { &self.value } pub fn country_code(&self) -> &str { &self.country_code } + pub fn tax_country_code(&self) -> &str { &self.tax_country_code } pub fn local_value(&self) -> &str { &self.local_value } pub fn id_type(&self) -> &str { &self.id_type } }