use std::collections::HashMap;
use lazy_static::lazy_static;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, ACCEPT, CONTENT_TYPE};
use roxmltree;
use serde_json::json;
use crate::verification::{Verifier, Verification, VerificationStatus::{*}, VerificationResponse, UnavailableReason::{*}};
use crate::errors::VerificationError;
use crate::TaxId;
static URI: &'static str = "https://www.uid-wse-a.admin.ch/V5.0/PublicServices.svc";
static ENVELOPE: &'static str = "
<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:uid=\"http://www.uid.admin.ch/xmlns/uid-wse\">
<soapenv:Header/>
<soapenv:Body>
<uid:ValidateVatNumber>
<uid:vatNumber>{value}</uid:vatNumber>
</uid:ValidateVatNumber>
</soapenv:Body>
</soapenv:Envelope>
";
const DATA_VALIDATION_FAILED: &str = "Data_validation_failed";
const REQUEST_LIMIT_EXCEEDED: &str = "Request_limit_exceeded";
lazy_static! {
#[derive(Debug)]
pub static ref HEADERS: HeaderMap = {
let mut headers = HeaderMap::new();
headers.insert(ACCEPT, HeaderValue::from_static("text/xml;charset=UTF-8"));
headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/xml;charset=UTF-8"));
headers.insert(
HeaderName::from_static("soapaction"),
HeaderValue::from_static("http://www.uid.admin.ch/xmlns/uid-wse/IPublicServices/ValidateVatNumber")
);
headers
};
}
#[derive(Debug)]
pub struct Bfs;
impl Bfs {
fn xml_to_hash(xml: &roxmltree::Document) -> HashMap<String, Option<String>> {
let mut hash = HashMap::new();
let tags_to_exclude = [
"Body",
"Envelope",
"Fault",
"businessFault",
"detail",
"ValidateVatNumberResponse",
];
for node in xml.descendants() {
let tag_name = node.tag_name().name();
if tag_name.trim().is_empty() || tags_to_exclude.contains(&tag_name) {
continue;
}
if let Some(text) = node.text() {
hash.insert(tag_name.to_string(), Some(text.to_string()));
}
}
hash
}
}
impl Verifier for Bfs {
fn make_request(&self, tax_id: &TaxId) -> Result<VerificationResponse, VerificationError> {
let client = reqwest::blocking::Client::new();
let body = ENVELOPE
.replace("{value}", tax_id.value());
let res = client
.post(URI)
.headers(HEADERS.clone())
.body(body)
.send()
.map_err(VerificationError::HttpError)?;
Ok(
VerificationResponse::new(
res.status().as_u16(),
res.text().map_err(VerificationError::HttpError)?
)
)
}
fn parse_response(&self, response: VerificationResponse) -> Result<Verification, VerificationError> {
let doc = roxmltree::Document::parse(response.body()).map_err(VerificationError::XmlParsingError)?;
let hash = Bfs::xml_to_hash(&doc);
let fault_string = hash.get("faultstring")
.and_then(|x| x.as_deref());
let status = match fault_string {
Some(DATA_VALIDATION_FAILED) => Unverified,
Some(REQUEST_LIMIT_EXCEEDED) => Unavailable(RateLimit),
Some(_) => return Err(VerificationError::UnexpectedResponse(
format!("Unexpected faultstring: {}", fault_string.unwrap().to_string())
)),
None => {
let result = hash.get("ValidateVatNumberResult").and_then(|x| x.as_deref());
match result {
Some("true") => Verified,
Some("false") => Unverified,
None | Some(_) => return Err(VerificationError::UnexpectedResponse(
"ValidateVatNumberResult should be 'true' or 'false'".to_string()
)),
}
},
};
Ok(Verification::new(status, json!(hash)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "ch_vat")]
#[test]
fn test_bfs_xml_to_hash() {
let xml = r#"
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ValidateVatNumberResponse xmlns="http://www.uid.admin.ch/xmlns/uid-wse">
<ValidateVatNumberResult>true</ValidateVatNumberResult>
</ValidateVatNumberResponse>
</s:Body>
</s:Envelope>
"#;
let doc = roxmltree::Document::parse(xml).unwrap();
let hash = Bfs::xml_to_hash(&doc);
assert_eq!(hash.get("ValidateVatNumberResult"), Some(&Some("true".to_string())));
}
#[cfg(feature = "ch_vat")]
#[test]
fn test_parse_response_verified() {
let response = VerificationResponse::new(
200,
r#"
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ValidateVatNumberResponse xmlns="http://www.uid.admin.ch/xmlns/uid-wse">
<ValidateVatNumberResult>true</ValidateVatNumberResult>
</ValidateVatNumberResponse>
</s:Body>
</s:Envelope>
"#.to_string()
);
let verifier = Bfs;
let verification = verifier.parse_response(response).unwrap();
assert_eq!(verification.status(), &Verified);
assert_eq!(verification.data(), &json!({
"ValidateVatNumberResult": "true"
}));
}
#[cfg(feature = "ch_vat")]
#[test]
fn test_parse_response_unverified() {
let response = VerificationResponse::new(
200,
r#"
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ValidateVatNumberResponse xmlns="http://www.uid.admin.ch/xmlns/uid-wse">
<ValidateVatNumberResult>false</ValidateVatNumberResult>
</ValidateVatNumberResponse>
</s:Body>
</s:Envelope>
"#.to_string()
);
let verifier = Bfs;
let verification = verifier.parse_response(response).unwrap();
assert_eq!(verification.status(), &Unverified);
assert_eq!(verification.data(), &json!({
"ValidateVatNumberResult": "false"
}));
}
#[cfg(feature = "ch_vat")]
#[test]
fn test_parse_response_unavailable() {
let response = VerificationResponse::new(
500,
r#"
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<s:Fault>
<faultcode>s:Client</faultcode>
<faultstring xml:lang="de-CH">Request_limit_exceeded</faultstring>
<detail>
<businessFault xmlns="http://www.uid.admin.ch/xmlns/uid-wse" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<operation xmlns="http://www.uid.admin.ch/xmlns/uid-wse-shared/2">ValidateVatNumber</operation>
<error xmlns="http://www.uid.admin.ch/xmlns/uid-wse-shared/2">Request_limit_exceeded</error>
<errorDetail xmlns="http://www.uid.admin.ch/xmlns/uid-wse-shared/2">Maximum number of 20 requests per 1 minute(s) exceeded</errorDetail>
</businessFault>
</detail>
</s:Fault>
</s:Body>
</s:Envelope>
"#.to_string()
);
let verifier = Bfs;
let verification = verifier.parse_response(response).unwrap();
assert_eq!(verification.status(), &Unavailable(RateLimit));
assert_eq!(verification.data(), &json!({
"error": REQUEST_LIMIT_EXCEEDED,
"errorDetail": "Maximum number of 20 requests per 1 minute(s) exceeded",
"operation": "ValidateVatNumber",
"faultcode": "s:Client",
"faultstring": REQUEST_LIMIT_EXCEEDED
}));
}
#[cfg(feature = "ch_vat")]
#[test]
fn test_parse_response_unavailable_unexpected_faultstring() {
let response = VerificationResponse::new(
500,
r#"
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<s:Fault>
<faultcode>s:Client</faultcode>
<faultstring xml:lang="de-CH">Unexpected_fault_string</faultstring>
</s:Fault>
</s:Body>
</s:Envelope>
"#.to_string()
);
let verifier = Bfs;
let verification = verifier.parse_response(response);
match verification {
Err(VerificationError::UnexpectedResponse(msg)) => {
assert_eq!(msg, "Unexpected faultstring: Unexpected_fault_string");
}
_ => panic!("Expected UnexpectedResponse error"),
}
}
#[cfg(feature = "ch_vat")]
#[test]
fn test_parse_response_unexpected_response_value() {
let response = VerificationResponse::new(
200,
r#"
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ValidateVatNumberResponse xmlns="http://www.uid.admin.ch/xmlns/uid-wse">
<ValidateVatNumberResult>unexpected value</ValidateVatNumberResult>
</ValidateVatNumberResponse>
</s:Body>
</s:Envelope>
"#.to_string()
);
let verifier = Bfs;
let verification = verifier.parse_response(response);
match verification {
Err(VerificationError::UnexpectedResponse(msg)) => {
assert_eq!(msg, "ValidateVatNumberResult should be 'true' or 'false'");
}
_ => panic!("Expected UnexpectedResponse error"),
}
}
#[cfg(feature = "ch_vat")]
#[test]
fn test_parse_response_empty_response_value() {
let response = VerificationResponse::new(
200,
r#"
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ValidateVatNumberResponse xmlns="http://www.uid.admin.ch/xmlns/uid-wse">
<ValidateVatNumberResult></ValidateVatNumberResult>
</ValidateVatNumberResponse>
</s:Body>
</s:Envelope>
"#.to_string()
);
let verifier = Bfs;
let verification = verifier.parse_response(response);
match verification {
Err(VerificationError::UnexpectedResponse(msg)) => {
assert_eq!(msg, "ValidateVatNumberResult should be 'true' or 'false'");
}
_ => panic!("Expected UnexpectedResponse error"),
}
}
}