Skip to content

Commit

Permalink
feat: vies lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
Mollemoll committed May 3, 2024
1 parent 20cfb7a commit f47fd92
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/eu_vat.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod syntax;
mod vies;

use lazy_static::lazy_static;
use syntax::EU_VAT_PATTERNS;
Expand Down
180 changes: 180 additions & 0 deletions src/eu_vat/vies.rs
Original file line number Diff line number Diff line change
@@ -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 = "
<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:v1=\"http://schemas.conversesolutions.com/xsd/dmticta/v1\">
<soapenv:Header/>
<soapenv:Body>
<checkVat xmlns=\"urn:ec.europa.eu:taxud:vies:services:checkVat:types\">
<countryCode>{country}</countryCode>
<vatNumber>{number}</vatNumber>
</checkVat>
</soapenv:Body>
</soapenv:Envelope>
";

pub struct VIES;

impl VIES {
fn xml_to_hash(xml: &roxmltree::Document) -> HashMap<String, String> {
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<String, VerificationError> {
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<Verification, VerificationError> {
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#"
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:v1="http://schemas.conversesolutions.com/xsd/dmticta/v1">
<soapenv:Header/>
<soapenv:Body>
<checkVat xmlns="urn:ec.europa.eu:taxud:vies:services:checkVat:types">
<countryCode>SE</countryCode>
<vatNumber>123456789101</vatNumber>
<requestDate>2021-01-01+01:00</requestDate>
<valid>true</valid>
<name>Test Company</name>
<address>Test Address</address>
</checkVat>
</soapenv:Body>
</soapenv:Envelope>
"#;
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#"
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:v1="http://schemas.conversesolutions.com/xsd/dmticta/v1">
<soapenv:Header/>
<soapenv:Body>
<checkVat xmlns="urn:ec.europa.eu:taxud:vies:services:checkVat:types">
<countryCode>SE</countryCode>
<vatNumber>123456789101</vatNumber>
<requestDate>2021-01-01+01:00</requestDate>
<valid>true</valid>
<name>Test Company</name>
<address>Test Address</address>
</checkVat>
</soapenv:Body>
</soapenv:Envelope>
"#;
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#"
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:v1="http://schemas.conversesolutions.com/xsd/dmticta/v1">
<soapenv:Header/>
<soapenv:Body>
<checkVat xmlns="urn:ec.europa.eu:taxud:vies:services:checkVat:types">
<countryCode>SE</countryCode>
<vatNumber>123456789101</vatNumber>
<requestDate>2021-01-01+01:00</requestDate>
<valid>false</valid>
<name>Test Company</name>
<address>Test Address</address>
</checkVat>
</soapenv:Body>
</soapenv:Envelope>
"#;
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:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<env:Header/>
<env:Body>
<env:Fault>
<faultcode>env:Server</faultcode>
<faultstring>MS_MAX_CONCURRENT_REQ</faultstring>
</env:Fault>
</env:Body>
</env:Envelope>
"#;
let verifier = VIES;
let verification = verifier.parse_response(response.to_string()).unwrap();

assert_eq!(*verification.status(), VerificationStatus::Unavailable);
}
}
4 changes: 2 additions & 2 deletions src/tax_id.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::process::id;
use crate::eu_vat::EUVat;
use crate::gb_vat::GBVat;
use crate::errors::ValidationError;
Expand All @@ -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,
Expand Down Expand Up @@ -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 }
}
Expand Down

0 comments on commit f47fd92

Please sign in to comment.