diff --git a/Cargo.toml b/Cargo.toml index d2965f2..074f286 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,12 @@ name = "tax_ids" version = "0.1.0" edition = "2021" +authors = ["Jonas Molander"] +description = "A library to validate and verify Tax Ids. Handle European, British, Norwegian or Swiss VAT numbers." license = "MIT OR Apache-2.0" +repository = "https://github.com/Mollemoll/tax-ids" +keywords = ["tax", "vat", "vies", "eu"] +categories = ["finance", "api-bindings", "localization", "parser-implementations"] [dependencies] lazy_static = "1.4.0" @@ -14,7 +19,6 @@ serde_json = "1.0.116" thiserror = "1.0.60" toml = { version = "0.8.12", optional = true } - [features] default = ["eu_vat"] eu_vat = ["roxmltree"] diff --git a/README.md b/README.md index 22ef3b5..b13ec45 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,159 @@ -# tax-ids +# Tax Ids -## License +This crate offers a solution for validating tax IDs (VAT/GST) for businesses operating within the European Union, +the United Kingdom, Switzerland, and Norway. + +Currently, the library provides the following functionalities: +- Validates the syntax of a tax ID against its type-specific regex pattern. +- Verifies the tax ID in the relevant government database (based on the tax ID type). + +The library has been inspired by the [valvat](https://github.com/yolk/valvat) library for Ruby. + +### Available features / tax id types + +| Feature | Description | Default | +|----------|--------------------|---------| +| `eu_vat` | European Union VAT | โœ“ | +| `gb_vat` | United Kingdom VAT | | +| `ch_vat` | Switzerland VAT | | +| `no_vat` | Norway VAT | | + +More info at [Tax Id Types](#tax-id-types). + +### Installation + +With default feature `eu_vat`: +```toml +[dependencies] +tax_ids = "0.1.0" +``` + +With `eu_vat` and `gb_vat` features enabled: +```toml +[dependencies] +tax_ids = { version = "0.1.0", features = ["eu_vat", "gb_vat"] } +``` + +## Usage + +```rust +use tax_ids::TaxId; +use tax_ids::VerificationStatus::{Verified, Unverified, Unavailable}; +use tax_ids::UnavailableReason::{ServiceUnavailable, Timeout, Block, RateLimit}; + +fn main() { + // Instantiate a new TaxId object. This can raise a ValidationError. + let tax_id = match TaxId::new("SE556703748501") { + Ok(tax_id) => tax_id, + Err(e) => { + println!("ValidationError: {}", e); + return; + } + }; + + assert_eq!(tax_id.value(), "SE556703748501"); + assert_eq!(tax_id.country_code(), "SE"); + assert_eq!(tax_id.tax_country_code(), "SE"); + assert_eq!(tax_id.local_value(), "556703748501"); + assert_eq!(tax_id.tax_id_type(), "eu_vat"); + + // The country code is the 2-char ISO code of the country. + // It's often the same as the tax country code, but not always. + // For example, the country code for Greece is GR, but EL for the Greek VAT number. + + // The United Kingdom has a country code GB and tax country code GB. + // However, due to Brexit, businesses in Northern Ireland + // have a country code GB but use VAT number/tax country code XI when trading + // with the EU. + + // Verification + + // Perform a verification request against the country's tax ID database. + // This can raise a VerificationError. + let verification = match tax_id.verify() { + Ok(verification) => verification, + Err(e) => { + println!("VerificationError: {}", e); + return; + } + }; + + assert_eq!(verification.status(), &Verified); + + // VerificationStatus can take one out of three different statuses: + // - Verified - The tax ID is legitimate. + // - Unverified - The tax ID is not legitimate. + // - Unavailable(UnavailableReason) - The verification couldn't be performed due to some reason. + + // These statuses are what you want to act upon. + match verification.status() { + Verified => { + // Proceed with payment + } + Unverified => { + // Ask the customer to provide a proper tax ID + } + Unavailable(reason) => { + // Process payment and verify the tax ID later? + + match reason { + ServiceUnavailable | Timeout => {}, + Block => { + // Adapt to your IP / VAT being blocked + } + RateLimit => { + // Consider how to avoid rate limiting + } + } + } + } + + // The full verification object: + + println!("{:?}", verification); + + // The data field is experimental and subject to change or removal. + // It will contain different data depending on what tax ID type is being verified. + // And what response the verification service provides. + + // Verification status: Verified + // Verification { + // performed_at: 2024-05-15T14:38:31.388914+02:00, + // status: Verified, + // data: Object { + // "address": String("REGERINGSGATAN 19 \n111 53 STOCKHOLM"), + // "countryCode": String("SE"), + // "name": String("Spotify AB"), + // "requestDate": String("2024-05-15+02:00"), + // "valid": String("true"), + // "vatNumber": String("556703748501" + // )} + // } +} +``` + +### Tax Id Types + +| Tax Id Type | Authority | Manual lookup | Documentation | +|-------------|-------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `eu_vat` | [VIES](https://ec.europa.eu/taxation_customs/vies/#/faq) | [๐Ÿ”](https://ec.europa.eu/taxation_customs/vies/) | [๐Ÿ“–](https://ec.europa.eu/taxation_customs/vies/#/technical-information) + [Availability](https://ec.europa.eu/taxation_customs/vies/#/help) | +| `gb_vat` | [HMRC](https://www.gov.uk/government/organisations/hm-revenue-customs) | [๐Ÿ”](https://www.tax.service.gov.uk/check-vat-number/enter-vat-details) | [๐Ÿ“–](https://developer.service.hmrc.gov.uk/api-documentation/docs/api/service/vat-registered-companies-api/1.0/oas/page) | +| `ch_vat` | [BFS](https://www.bfs.admin.ch/bfs/en/home/registers/enterprise-register/business-enterprise-register.html) | [๐Ÿ”](https://www.uid.admin.ch/Search.aspx?lang=en) | [๐Ÿ“–](https://www.bfs.admin.ch/bfs/fr/home/registres/registre-entreprises/numero-identification-entreprises/registre-ide/interfaces-ide.assetdetail.11007266.html) | +| `no_vat` | [Brรธnnรธysundregistrene](https://www.brreg.no/) | [๐Ÿ”](https://data.brreg.no/enhetsregisteret/oppslag/enheter) | [๐Ÿ“–](https://data.brreg.no/enhetsregisteret/api/dokumentasjon/no/index.html#tag/Enheter/operation/hentEnhet) | + +### License Licensed under either of -* Apache License, Version 2.0 - ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) -* MIT license - ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +- Apache License, Version 2.0 + ([LICENSE-APACHE](https://github.com/Mollemoll/tax-ids?tab=Apache-2.0-1-ov-file) or ) +- MIT license + ([LICENSE-MIT](https://github.com/Mollemoll/tax-ids?tab=MIT-2-ov-file) or ) at your option. -## Contribution +### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be -dual licensed as above, without any additional terms or conditions. \ No newline at end of file +dual licensed as above, without any additional terms or conditions. diff --git a/src/errors.rs b/src/errors.rs index 55240e8..96875a4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -4,9 +4,11 @@ use std::fmt::Debug; #[derive(thiserror::Error, Debug, PartialEq)] pub enum ValidationError { #[error("Country code {0} is not supported")] + /// The country code is not supported UnsupportedCountryCode(String), #[error("Invalid syntax")] + /// The syntax of the tax id is invalid for the given country InvalidSyntax, } diff --git a/src/lib.rs b/src/lib.rs index 6674eb7..fd9ef14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![doc = include_str!("../README.md")] + mod errors; mod verification; mod syntax; @@ -66,6 +68,8 @@ impl fmt::Debug for TaxId { } impl TaxId { + /// Use this associated function to validate the syntax of a given tax id number against + /// its country-specific regex pattern without creating any TaxId. pub fn validate_syntax(value: &str) -> Result<(), ValidationError> { let tax_country_code = &value[0..2]; SYNTAX.get(tax_country_code) @@ -79,6 +83,9 @@ impl TaxId { }) } + /// Constructs a TaxId after validating its syntax based on the country-specific regex pattern. + /// If the syntax validation is successful, the returned TaxId can be used for further + /// verification against the corresponding government database. pub fn new(value: &str) -> Result { let tax_country_code = &value[0..2]; let local_value = &value[2..]; @@ -106,15 +113,28 @@ impl TaxId { }) } + /// Performs a request to verify the tax id against the corresponding government database. pub fn verify(&self) -> Result { self.id_type.verifier().verify(self) } + /// Returns the full tax id value. IE: SE556703748501 pub fn value(&self) -> &str { &self.value } + /// Returns the country code. IE: SE pub fn country_code(&self) -> &str { &self.country_code } + /// Returns the tax country code. IE: SE + /// + /// This is the same as the country code for most countries, but not for XI and EL. + /// + /// XI is the tax country code that Northern Ireland business (the United Kingdom) should use + /// while trading with the EU. A consequence of Brexit. + /// + /// EL is the tax country code for Greece. pub fn tax_country_code(&self) -> &str { &self.tax_country_code } + /// Returns the local value of the tax id. IE: 556703748501 pub fn local_value(&self) -> &str { &self.local_value } + /// Returns the type of tax id in snake_case. IE: eu_vat, gb_vat, ch_va or no_vat pub fn tax_id_type(&self) -> &str { self.id_type.name() } fn id_type(&self) -> &Box { &self.id_type } } diff --git a/src/tax_id.rs b/src/tax_id.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/verification.rs b/src/verification.rs index 3bd314c..e2feb0a 100644 --- a/src/verification.rs +++ b/src/verification.rs @@ -22,8 +22,11 @@ impl VerificationResponse { #[derive(Debug, PartialEq, Clone, Copy)] pub enum VerificationStatus { + /// Represents a successful verification where the government database confirmed the ID as legitimate. Verified, + /// Represents an unsuccessful verification where the government database identified the ID as illegitimate. Unverified, + /// Represents a case where verification was not possible due to certain reasons (e.g., government database was unavailable). Unavailable(UnavailableReason), } @@ -43,6 +46,7 @@ pub struct Verification { } impl Verification { + #[doc(hidden)] pub fn new(status: VerificationStatus, data: serde_json::Value) -> Verification { Verification { performed_at: Local::now(), @@ -51,7 +55,21 @@ impl Verification { } } + /// This VerificationStatus is what the crate user should use to determine how to proceed. + /// + /// A checkout example: + /// - Enable/process the transaction upon `VerificationStatus::Verified`. + /// - Block transaction/provide a validation msg upon `VerificationStatus::Unverified`. + /// - Enable/process the transaction upon `VerificationStatus::Unavailable` but perform a + /// re-verification at a later stage. pub fn status(&self) -> &VerificationStatus { &self.status } + /// Additional data selected by the crate owner from the government database response. + /// This data can be used to provide more context about the verification. + /// The data is in JSON format. + /// + /// Includes error details in case of an unsuccessful verification. + /// + /// Subject to change in future versions. pub fn data(&self) -> &serde_json::Value { &self.data } }