VAT numbers
EU VAT numbers checked offline by format and check digits for all 27 countries, Northern Ireland, the UK, Switzerland and Norway, to catch typos before an invoice goes out.
Version 1.0.0 · 3 functions · 0 crates
Functions
What use vat.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use vat_check, vat_valid from vat (Quillo 0.6).
vat_check(number: str) → anyor an errorWhat a VAT number is: { valid, country, number }, number written the standard way ("DE136695976", Greece as "EL"), spaces, dots and dashes taken off. valid means its form and check digits are right: a typo is caught, but whether the number is registered is only for the tax office (the EU's VIES service) to say. An error for a country with no VAT rules here.
vat_valid(number: str) → boolWhether number is a well-formed VAT number with right check digits; false for a country with no rules here.
vat_countries() → listThe countries whose numbers can be checked, as their VAT prefixes: the 27 EU countries (Greece is "EL"), "XI" for Northern Ireland, "GB", "CH" and "NO".
Get it
$ quillo install vat # vat.qwer into this folder$ quillo install vat --example # and its example, vat.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/vat.qwer$ curl -O https://quillolang.org/plugins/files/vat.qweThen, in your program, all of it or only what you call:
use vat.qweruse vat_check, vat_valid from vat // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// A B2B checkout that checks the buyer's VAT number for typos before it goes on the invoice.//// quillo run vat.qwe then open http://localhost:8080/vat?number=DE%20136%20695%20976use "vat.qwer"route GET /vat ? { number: str } = vat_check(number)?route GET /examples = ["DE136695976", "ATU13585627", "FR40303265045", "IE6388047V", "LU26375245", "NL004495445B01", "PL5260001246", "DE136695977", "FR41303265045"] | map((n) => { number: n, valid: vat_valid(n) })route POST /invoice { let vat = request.json?.vat ?? "" if !vat_valid(vat) { reply 422 { error: "That VAT number does not look right: check it for typos." } } { ok: true, vat: vat_check(vat)?.number }}$ quillo run vat.qwe$ curl 'localhost:8080/vat?number=DE%20136%20695%20976'$ curl 'localhost:8080/examples'Source
//! EU VAT numbers checked offline by their format and check digits, for every EU country plus Northern Ireland, the UK, Switzerland and Norway: catch typos at checkout before an invoice goes out.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};fn d(s: &str) -> Vec<u32> { s.chars().filter_map(|c| c.to_digit(10)).collect()}fn all_digits(s: &str, n: &[usize]) -> bool { n.contains(&s.len()) && s.chars().all(|c| c.is_ascii_digit())}fn weighted(ds: &[u32], w: &[u32]) -> u32 { ds.iter().zip(w).map(|(a, b)| a * b).sum()}fn luhn(ds: &[u32]) -> bool { ds.iter().rev().enumerate().map(|(i, &n)| if i % 2 == 1 { let x = n * 2; if x > 9 { x - 9 } else { x } } else { n }).sum::<u32>() % 10 == 0}/// ISO 7064 MOD 11,10, used by Germany and Croatia.fn mod11_10(ds: &[u32]) -> u32 { let mut p = 10; for &x in ds { let mut s = (x + p) % 10; if s == 0 { s = 10; } p = (2 * s) % 11; } (11 - p) % 10}fn modn(s: &str, m: u64) -> u64 { s.chars().fold(0u64, |r, c| (r * 10 + c.to_digit(10).unwrap_or(0) as u64) % m)}/// Whether the number part `n` (no country prefix) passes country `cc`'s rules; Err for a country we do not know.fn check(cc: &str, n: &str) -> Result<bool, String> { let ds = d(n); Ok(match cc { "AT" => { let Some(r) = n.strip_prefix('U') else { return Ok(false) }; if !all_digits(r, &[8]) { return Ok(false) } let x = d(r); let s: u32 = x[..7].iter().enumerate().map(|(i, &v)| if i % 2 == 1 { let y = v * 2; y / 10 + y % 10 } else { v }).sum(); (10 - (s + 4) % 10) % 10 == x[7] } "BE" => { let n = if n.len() == 9 { format!("0{}", n) } else { n.to_string() }; all_digits(&n, &[10]) && (n.starts_with('0') || n.starts_with('1')) && 97 - modn(&n[..8], 97) == n[8..].parse::<u64>().unwrap_or(0) } "BG" => { if all_digits(n, &[9]) { let mut c = weighted(&ds[..8], &[1, 2, 3, 4, 5, 6, 7, 8]) % 11; if c == 10 { c = weighted(&ds[..8], &[3, 4, 5, 6, 7, 8, 9, 10]) % 11 % 10 } c == ds[8] } else if all_digits(n, &[10]) { // A person's number (EGN), a foreigner's, or another. let egn = weighted(&ds[..9], &[2, 4, 8, 5, 10, 9, 7, 3, 6]) % 11 % 10 == ds[9]; let foreign = weighted(&ds[..9], &[21, 19, 17, 13, 11, 9, 7, 3, 1]) % 10 == ds[9]; let other = { let r = 11 - weighted(&ds[..9], &[4, 3, 2, 7, 6, 5, 4, 3, 2]) % 11; r != 10 && r % 11 == ds[9] }; egn || foreign || other } else { false } } "CY" => { if n.len() != 9 || !all_digits(&n[..8], &[8]) || n.starts_with('2') { return Ok(false) } let table = [1, 0, 5, 7, 9, 13, 15, 17, 19, 21]; let s: u32 = ds[..8].iter().enumerate().map(|(i, &v)| if i % 2 == 0 { table[v as usize] } else { v }).sum(); n.as_bytes()[8] == b'A' + (s % 26) as u8 } "CZ" => { if all_digits(n, &[8]) { let a = 11 - weighted(&ds[..7], &[8, 7, 6, 5, 4, 3, 2]) % 11; n.as_bytes()[0] != b'9' && (a % 10) == ds[7] } else if all_digits(n, &[10]) { modn(n, 11) == 0 } else { all_digits(n, &[9]) } } "DE" => all_digits(n, &[9]) && !n.starts_with('0') && mod11_10(&ds[..8]) == ds[8], "DK" => all_digits(n, &[8]) && !n.starts_with('0') && weighted(&ds, &[2, 7, 6, 5, 4, 3, 2, 1]) % 11 == 0, "EE" => all_digits(n, &[9]) && n.starts_with("10") && (10 - weighted(&ds[..8], &[3, 7, 1, 3, 7, 1, 3, 7]) % 10) % 10 == ds[8], "EL" => all_digits(n, &[9]) && weighted(&ds[..8], &[256, 128, 64, 32, 16, 8, 4, 2]) % 11 % 10 == ds[8], "ES" => { if n.len() != 9 { return Ok(false) } let (f, mid, last) = (n.as_bytes()[0] as char, &n[1..8], n.as_bytes()[8] as char); let dni = "TRWAGMYFPDXBNJZSQVHLCKE"; if f.is_ascii_digit() && all_digits(&n[..8], &[8]) { dni.as_bytes()[(n[..8].parse::<u64>().unwrap_or(0) % 23) as usize] as char == last } else if "XYZ".contains(f) && all_digits(mid, &[7]) { let num = format!("{}{}", "XYZ".find(f).unwrap_or(0), mid); dni.as_bytes()[(num.parse::<u64>().unwrap_or(0) % 23) as usize] as char == last } else if "KLM".contains(f) && all_digits(mid, &[7]) { dni.as_bytes()[(mid.parse::<u64>().unwrap_or(0) % 23) as usize] as char == last } else if "ABCDEFGHJNPQRSUVW".contains(f) && all_digits(mid, &[7]) { let m = d(mid); let s: u32 = m.iter().enumerate().map(|(i, &v)| if i % 2 == 0 { let y = v * 2; y / 10 + y % 10 } else { v }).sum(); let c = (10 - s % 10) % 10; last == char::from_digit(c, 10).unwrap_or(' ') || last == "JABCDEFGHI".as_bytes()[c as usize] as char } else { false } } "FI" => { if !all_digits(n, &[8]) { return Ok(false) } let r = weighted(&ds[..7], &[7, 9, 10, 5, 8, 4, 2]) % 11; r != 1 && (if r == 0 { 0 } else { 11 - r }) == ds[7] } "FR" => { if n.len() != 11 || !all_digits(&n[2..], &[9]) { return Ok(false) } let key = &n[..2]; if all_digits(key, &[2]) { (12 + 3 * modn(&n[2..], 97)) % 97 == key.parse::<u64>().unwrap_or(999) } else { // Newer keys hold letters (not I or O); only their form is checked. key.chars().all(|c| c.is_ascii_alphanumeric() && c != 'I' && c != 'O') } } "HR" => all_digits(n, &[11]) && mod11_10(&ds[..10]) == ds[10], "HU" => all_digits(n, &[8]) && (10 - weighted(&ds[..7], &[9, 7, 3, 1, 9, 7, 3]) % 10) % 10 == ds[7], "IE" => { let b = n.as_bytes(); // The old form, 1A23456B, is the new one 0234561B. let n = if n.len() == 8 && b[0].is_ascii_digit() && (b[1].is_ascii_uppercase() || b[1] == b'+' || b[1] == b'*') && all_digits(&n[2..7], &[5]) { format!("0{}{}{}", &n[2..7], b[0] as char, b[7] as char) } else { n.to_string() }; if !(n.len() == 8 || n.len() == 9) || !all_digits(&n[..7], &[7]) { return Ok(false) } let letters = "WABCDEFGHIJKLMNOPQRSTUV"; let mut s = weighted(&d(&n[..7]), &[8, 7, 6, 5, 4, 3, 2]); if n.len() == 9 { let extra = n.as_bytes()[8] as char; s += 9 * letters.find(extra).unwrap_or(99) as u32; } letters.as_bytes()[(s % 23) as usize] == n.as_bytes()[7] } "IT" => all_digits(n, &[11]) && luhn(&ds), "LT" => { let w1: [u32; 11] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2]; let w2: [u32; 11] = [3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4]; if !all_digits(n, &[9, 12]) || ds[ds.len() - 2] != 1 { return Ok(false) } let k = ds.len() - 1; let mut c = weighted(&ds[..k], &w1[..k]) % 11; if c == 10 { c = weighted(&ds[..k], &w2[..k]) % 11 % 10 } c == ds[k] } "LU" => all_digits(n, &[8]) && modn(&n[..6], 89) == n[6..].parse::<u64>().unwrap_or(999), "LV" => { if !all_digits(n, &[11]) { return Ok(false) } if ds[0] > 3 { let r = 3 - (weighted(&ds[..10], &[9, 1, 4, 8, 3, 10, 2, 5, 7, 6]) % 11) as i32; let r = if r < -1 { r + 11 } else { r }; r >= 0 && r as u32 == ds[10] } else { // A person's code starts with the birth date, DDMMYY. let (dd, mm) = (ds[0] * 10 + ds[1], ds[2] * 10 + ds[3]); (1..=31).contains(&dd) && (1..=12).contains(&mm) } } "MT" => all_digits(n, &[8]) && !n.starts_with('0') && 37 - weighted(&ds[..6], &[3, 4, 6, 7, 8, 9]) % 37 == n[6..].parse::<u32>().unwrap_or(999), "NL" => { if n.len() != 12 || !all_digits(&n[..9], &[9]) || n.as_bytes()[9] != b'B' || !all_digits(&n[10..], &[2]) { return Ok(false) } let x = d(&n[..9]); let s = weighted(&x[..8], &[9, 8, 7, 6, 5, 4, 3, 2]) % 11; // Sole traders' numbers since 2020 use mod 97 over "NL" and the whole number, letters as numbers. let digits: String = format!("NL{}", n).chars().map(|c| if c.is_ascii_digit() { c.to_string() } else { (c as u32 - 'A' as u32 + 10).to_string() }).collect(); (s != 10 && s == x[8]) || modn(&digits, 97) == 1 } "PL" => all_digits(n, &[10]) && weighted(&ds[..9], &[6, 5, 7, 2, 3, 4, 5, 6, 7]) % 11 == ds[9], "PT" => { if !all_digits(n, &[9]) { return Ok(false) } let c = 11 - weighted(&ds[..8], &[9, 8, 7, 6, 5, 4, 3, 2]) % 11; (if c > 9 { 0 } else { c }) == ds[8] } "RO" => { if !(2..=10).contains(&n.len()) || !n.chars().all(|c| c.is_ascii_digit()) || n.starts_with('0') { return Ok(false) } let body = format!("{:0>9}", &n[..n.len() - 1]); let c = weighted(&d(&body), &[7, 5, 3, 2, 1, 7, 5, 3, 2]) * 10 % 11 % 10; c == ds[ds.len() - 1] } "SE" => all_digits(n, &[12]) && n.ends_with("01") && luhn(&ds[..10]), "SI" => { if !all_digits(n, &[8]) || n.starts_with('0') { return Ok(false) } let c = 11 - weighted(&ds[..7], &[8, 7, 6, 5, 4, 3, 2]) % 11; c != 11 && c % 10 == ds[7] } "SK" => all_digits(n, &[10]) && !n.starts_with('0') && [2, 3, 4, 7, 8, 9].contains(&ds[2]) && modn(n, 11) == 0, "GB" | "XI" => { if n.starts_with("GD") || n.starts_with("HA") { all_digits(&n[2..], &[3]) } else if all_digits(n, &[9, 12]) { let s = weighted(&ds[..7], &[8, 7, 6, 5, 4, 3, 2]) + ds[7] * 10 + ds[8]; s % 97 == 0 || (s + 55) % 97 == 0 } else { false } } "CH" => { let n = n.trim_end_matches("MWST").trim_end_matches("TVA").trim_end_matches("IVA"); let Some(r) = n.strip_prefix('E') else { return Ok(false) }; if !all_digits(r, &[9]) { return Ok(false) } let x = d(r); let c = 11 - weighted(&x[..8], &[5, 4, 3, 2, 7, 6, 5, 4]) % 11; c != 10 && c % 11 == x[8] } "NO" => { let n = n.trim_end_matches("MVA"); if !all_digits(n, &[9]) { return Ok(false) } let x = d(n); let c = 11 - weighted(&x[..8], &[3, 2, 7, 6, 5, 4, 3, 2]) % 11; c != 10 && c % 11 == x[8] } other => return Err(format!("no VAT rules for {:?}", other)), })}fn split(number: &str) -> Result<(String, String), String> { let s: String = number.chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>().to_uppercase(); if s.len() < 4 || s.len() > 20 { return Err("a VAT number starts with its two-letter country code".into()); } let (cc, rest) = s.split_at(2); let cc = match cc { "GR" => "EL", c => c }; // Switzerland writes CHE-123.456.789: "CH" and the rest start with "E". Ok((cc.to_string(), rest.to_string()))}/// What a VAT number is: `{ valid, country, number }`, `number` written the standard way ("DE136695976", Greece as "EL"), spaces, dots and dashes taken off. `valid` means its form and check digits are right: a typo is caught, but whether the number is registered is only for the tax office (the EU's VIES service) to say. An error for a country with no VAT rules here.pub fn vat_check(number: &str) -> Result<Value, String> { let (cc, rest) = split(number)?; let ok = check(&cc, &rest)?; Ok(json!({ "valid": ok, "country": cc, "number": format!("{}{}", cc, rest) }))}/// Whether `number` is a well-formed VAT number with right check digits; false for a country with no rules here.pub fn vat_valid(number: &str) -> bool { split(number).and_then(|(c, r)| check(&c, &r)).unwrap_or(false)}/// The countries whose numbers can be checked, as their VAT prefixes: the 27 EU countries (Greece is "EL"), "XI" for Northern Ireland, "GB", "CH" and "NO".pub fn vat_countries() -> Vec<String> { "AT BE BG CY CZ DE DK EE EL ES FI FR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK XI GB CH NO".split(' ').map(String::from).collect()}Related
tax_ids.qwer · 1.0.0Tax and business numbers outside the EU checked by each country's rules: EIN, ABN and TFN, IRD, GSTIN and PAN, CNPJ, CUIT, RUT, NIT, RUC, RFC, BN, Japan's corporate number, BRN, USCC, UBN, INN and UEN, with what each says about its holder.7 views · 7 downloadsCard numbers cards.qwer · 1.0.0Payment card numbers checked before they reach a payment provider: Luhn check digit, brand and length, expiry, and the number formatted or masked for receipts.7 views · 7 downloadsNational ID numbers national_ids.qwer · 1.0.0National identity numbers of 29 countries checked by their own rules (US SSN, UK NINO, CPF, Aadhaar, PH TIN, DNI and NIE, codice fiscale, PESEL...), with the birth date and sex read out where the number holds them.7 views · 7 downloadsPostal codes postal_codes.qwer · 1.0.0Postal codes checked for 45 countries and written the post office's way: SW1A 1AA, 1012 JS, K1A 0B1, 100-0001, 20500-0001.6 views · 7 downloadsVINs vin.qwer · 1.0.0Vehicle identification numbers checked and decoded: the check digit, the maker's region, country and name, the model year, plant and serial.9 views · 8 downloadsEDIFACT edifact.qwer · 1.0.0UN/EDIFACT for European retail and logistics: interchanges parsed and checked, ORDERS, INVOIC and DESADV read into parties, lines and totals, interchanges built with escapes and counts, and CONTRL acknowledgments.8 views · 9 downloadsThread 0 posts
On VAT numbers: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.