Validation

Tax and business numbers

Tax 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.

Version 1.0.0 · 3 functions · 1 crate

#tax#validation#b2b#invoices

Functions

What use tax_ids.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use tax_id_check, tax_id_guess from tax_ids (Quillo 0.6).

tax_id_check(country: str, kind: str, number: str) → any

Checks a tax or business number: country (ISO code) and kind ("EIN", "ABN", "TFN", "IRD", "GSTIN", "PAN", "CNPJ", "CUIT", "RUT", "NIT", "RUC", "RFC", "BN", "CN", "BRN", "USCC", "UBN", "TIN", "INN", "UEN"; "" for the country's first). Gives { valid, country, type, name, formatted, holder, note, error }; holder says what kind of taxpayer the number marks, where it does. Spaces, dots, dashes and slashes are ignored. EU VAT numbers are the vat plugin's.

tax_id_guess(number: str) → list

Every kind of number number could validly be, for a form that does not ask the country: [{ country, type, name, formatted }].

tax_id_kinds() → list

The numbers with rules: [{ country, type, name }].

Get it

$ quillo install tax_ids               # tax_ids.qwer into this folder$ quillo install tax_ids --example     # and its example, tax_ids.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/tax_ids.qwer$ curl -O https://quillolang.org/plugins/files/tax_ids.qwe

Then, in your program, all of it or only what you call:

main.qwe
use tax_ids.qweruse tax_id_check, tax_id_guess from tax_ids      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }

Example

tax_ids.qwe
// Supplier onboarding for a marketplace: each supplier's tax number checked by its country's// rules before the first payout, and what the number says about them shown.////   quillo run tax_ids.qwe      then open http://localhost:8080/suppliers and http://localhost:8080/check/AU/ABN/51%20824%20753%20556use "tax_ids.qwer"route GET /check/:country/:kind/*rest = tax_id_check(country, kind, rest)let SUPPLIERS = [    { name: "Pacific Teas", country: "AU", kind: "ABN", number: "51 824 753 556" },    { name: "Ganesh Exports", country: "IN", kind: "GSTIN", number: "27AAPFU0939F1ZV" },    { name: "Café do Sul", country: "BR", kind: "CNPJ", number: "11.222.333/0001-81" },    { name: "Andes Coffee", country: "CO", kind: "NIT", number: "890903938-8" },    { name: "Hangzhou Silk", country: "CN", kind: "USCC", number: "91350100M000100Y43" },    { name: "Seoul Ceramics", country: "KR", kind: "BRN", number: "220-81-62517" },    { name: "Typo Ltd", country: "AU", kind: "ABN", number: "51 824 753 557" },    { name: "Acme Inc", country: "US", kind: "EIN", number: "07-1234567" },]route GET /suppliers = SUPPLIERS | map((s) => { let r = tax_id_check(s.country, s.kind, s.number); `${s.name}: ${r.valid ? r.formatted + (r.holder != null ? " (" + r.holder + ")" : "") : r.error}` })route GET /guess = {    nine_digits: tax_id_guess("123456782") | map((g) => `${g.country} ${g.type}`),    india: tax_id_check("IN", "GSTIN", "27AAPFU0939F1ZV"),    kinds: len(tax_id_kinds()),}
$ quillo run tax_ids.qwe$ curl 'localhost:8080/suppliers'$ curl 'localhost:8080/guess'

Source

tax_ids.qwer
//! Business and tax numbers outside the EU checked by each country's rules: US EIN, Australia's ABN and TFN, New Zealand's IRD, India's GSTIN and PAN, Brazil's CNPJ, Argentina's CUIT, Chile's RUT, Colombia's NIT, Peru's RUC, Mexico's RFC, Canada's BN, Japan's corporate number, Korea's BRN, China's USCC, Taiwan's UBN, Thailand's TIN, Russia's INN and Singapore's UEN; with what the number says about its holder.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Map, Value};struct Info {    kind: &'static str,    formatted: String,    holder: Option<String>,    note: Option<String>,}type R = Result<Info, String>;const BAD_CHECK: &str = "the check digit does not match: a typo";fn info(kind: &'static str, formatted: String) -> Info {    Info { kind, formatted, holder: None, note: None }}fn clean(s: &str) -> String {    s.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '&' || *c == 'Ñ').map(|c| c.to_ascii_uppercase()).collect()}fn d(s: &str) -> Vec<u32> {    s.chars().filter_map(|c| c.to_digit(10)).collect()}fn digits(s: &str, lens: &[usize]) -> Result<(), String> {    if lens.contains(&s.len()) && s.chars().all(|c| c.is_ascii_digit()) {        Ok(())    } else {        Err(format!("{} digits are expected", lens.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(" or ")))    }}fn w(ds: &[u32], ws: &[u32]) -> u32 {    ds.iter().zip(ws).map(|(a, b)| a * b).sum()}fn luhn(s: &str) -> bool {    s.chars().rev().enumerate().map(|(i, c)| { let v = c.to_digit(10).unwrap_or(0); if i % 2 == 1 { let x = v * 2; if x > 9 { x - 9 } else { x } } else { v } }).sum::<u32>() % 10 == 0}fn us_ein(s: &str) -> R {    digits(s, &[9])?;    const PREFIXES: &[u32] = &[1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 71, 72, 73, 74, 75, 76, 77, 80, 81, 82, 83, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 98, 99];    let p: u32 = s[..2].parse().unwrap_or(0);    if !PREFIXES.contains(&p) {        return Err(format!("the IRS gives out no EIN starting with {:02}", p));    }    let mut i = info("EIN (employer identification number)", format!("{}-{}", &s[..2], &s[2..]));    i.note = Some(match p { 20 | 26 | 27 | 45 | 46 | 47 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 92 | 93 | 98 | 99 => "issued online or by a service center", _ => "issued by an IRS campus" }.into());    Ok(i)}fn au_abn(s: &str) -> R {    digits(s, &[11])?;    let mut x = d(s);    if x[0] == 0 {        return Err("an ABN does not start with 0".into());    }    x[0] -= 1;    if w(&x, &[10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19]) % 89 != 0 {        return Err(BAD_CHECK.into());    }    Ok(info("ABN (Australian business number)", format!("{} {} {} {}", &s[..2], &s[2..5], &s[5..8], &s[8..])))}fn au_tfn(s: &str) -> R {    digits(s, &[8, 9])?;    let ok = if s.len() == 9 { w(&d(s), &[1, 4, 3, 7, 5, 8, 6, 9, 10]) % 11 == 0 } else { w(&d(s), &[10, 7, 8, 4, 6, 3, 5, 1]) % 11 == 0 };    if !ok {        return Err(BAD_CHECK.into());    }    Ok(info("TFN (tax file number)", if s.len() == 9 { format!("{} {} {}", &s[..3], &s[3..6], &s[6..]) } else { s.to_string() }))}fn nz_ird(s: &str) -> R {    digits(s, &[8, 9])?;    let p = format!("{:0>9}", s);    let n: u64 = p.parse().unwrap_or(0);    if !(10_000_000..=150_000_000).contains(&n) {        return Err("an IRD number is between 10-000-000 and 150-000-000".into());    }    let x = d(&p);    let check = |ws: &[u32]| { let r = w(&x[..8], ws) % 11; if r == 0 { 0 } else { 11 - r } };    let mut c = check(&[3, 2, 7, 6, 5, 4, 3, 2]);    if c == 10 {        c = check(&[7, 4, 3, 2, 5, 2, 7, 6]);    }    if c != x[8] {        return Err(BAD_CHECK.into());    }    Ok(info("IRD number (and GST number)", format!("{}-{}-{}", &p[..3], &p[3..6], &p[6..])))}const PAN_HOLDERS: &[(char, &str)] = &[('P', "a person"), ('C', "a company"), ('H', "a Hindu undivided family"), ('F', "a firm or LLP"), ('A', "an association of persons"), ('T', "a trust"), ('B', "a body of individuals"), ('L', "a local authority"), ('J', "an artificial juridical person"), ('G', "a government body")];fn pan_check(p: &str) -> Result<String, String> {    let c: Vec<char> = p.chars().collect();    if c.len() != 10 || !c[..5].iter().all(|x| x.is_ascii_alphabetic()) || !c[5..9].iter().all(|x| x.is_ascii_digit()) || !c[9].is_ascii_alphabetic() {        return Err("a PAN is 5 letters, 4 digits and a letter".into());    }    PAN_HOLDERS.iter().find(|(k, _)| *k == c[3]).map(|(_, h)| h.to_string()).ok_or_else(|| format!("`{}` is not a PAN holder type", c[3]))}fn in_pan(s: &str) -> R {    let holder = pan_check(s)?;    let mut i = info("PAN (permanent account number)", s.to_string());    i.holder = Some(holder);    Ok(i)}const STATES: &[(&str, &str)] = &[("01", "Jammu and Kashmir"), ("02", "Himachal Pradesh"), ("03", "Punjab"), ("04", "Chandigarh"), ("05", "Uttarakhand"), ("06", "Haryana"), ("07", "Delhi"), ("08", "Rajasthan"), ("09", "Uttar Pradesh"), ("10", "Bihar"), ("11", "Sikkim"), ("12", "Arunachal Pradesh"), ("13", "Nagaland"), ("14", "Manipur"), ("15", "Mizoram"), ("16", "Tripura"), ("17", "Meghalaya"), ("18", "Assam"), ("19", "West Bengal"), ("20", "Jharkhand"), ("21", "Odisha"), ("22", "Chhattisgarh"), ("23", "Madhya Pradesh"), ("24", "Gujarat"), ("26", "Dadra and Nagar Haveli and Daman and Diu"), ("27", "Maharashtra"), ("29", "Karnataka"), ("30", "Goa"), ("31", "Lakshadweep"), ("32", "Kerala"), ("33", "Tamil Nadu"), ("34", "Puducherry"), ("35", "Andaman and Nicobar Islands"), ("36", "Telangana"), ("37", "Andhra Pradesh"), ("38", "Ladakh"), ("97", "Other territory"), ("99", "Centre jurisdiction")];fn in_gstin(s: &str) -> R {    const AL: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";    if s.len() != 15 {        return Err("a GSTIN has 15 characters".into());    }    let state = STATES.iter().find(|(c, _)| *c == &s[..2]).map(|(_, n)| *n).ok_or_else(|| format!("`{}` is not a state code", &s[..2]))?;    let holder = pan_check(&s[2..12])?;    let sum: usize = s[..14].chars().enumerate().map(|(i, c)| { let v = AL.find(c).unwrap_or(0) * if i % 2 == 0 { 1 } else { 2 }; v / 36 + v % 36 }).sum();    if AL.chars().nth((36 - sum % 36) % 36) != s.chars().nth(14) {        return Err(BAD_CHECK.into());    }    let mut i = info("GSTIN", s.to_string());    i.holder = Some(holder);    i.note = Some(format!("registered in {}, PAN {}, registration {}", state, &s[2..12], &s[12..13]));    Ok(i)}fn br_cnpj(s: &str) -> R {    digits(s, &[14])?;    let x = d(s);    let c = |ws: &[u32], n: usize| { let r = w(&x[..n], ws) % 11; if r < 2 { 0 } else { 11 - r } };    if c(&[5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2], 12) != x[12] || c(&[6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2], 13) != x[13] {        return Err(BAD_CHECK.into());    }    let mut i = info("CNPJ", format!("{}.{}.{}/{}-{}", &s[..2], &s[2..5], &s[5..8], &s[8..12], &s[12..]));    i.note = Some(if &s[8..12] == "0001" { "the head office".into() } else { format!("branch {}", &s[8..12]) });    Ok(i)}fn ar_cuit(s: &str) -> R {    digits(s, &[11])?;    let x = d(s);    let r = 11 - w(&x[..10], &[5, 4, 3, 2, 7, 6, 5, 4, 3, 2]) % 11;    let c = match r { 11 => 0, 10 => 9, r => r };    if c != x[10] {        return Err(BAD_CHECK.into());    }    let mut i = info("CUIT / CUIL", format!("{}-{}-{}", &s[..2], &s[2..10], &s[10..]));    i.holder = Some(match &s[..2] { "20" => "a man", "27" => "a woman", "23" | "24" | "25" | "26" => "a person", "30" | "33" | "34" => "a company", _ => "unknown" }.into());    Ok(i)}fn cl_rut(s: &str) -> R {    let (body, check) = s.split_at(s.len().saturating_sub(1));    if body.is_empty() || body.len() > 9 || !body.chars().all(|c| c.is_ascii_digit()) {        return Err("a RUT is up to 9 digits and a check digit or K".into());    }    let (mut sum, mut m) = (0, 2);    for c in body.chars().rev() {        sum += c.to_digit(10).unwrap() * m;        m = if m == 7 { 2 } else { m + 1 };    }    let expect = match 11 - sum % 11 { 11 => "0".to_string(), 10 => "K".to_string(), r => r.to_string() };    if expect != check {        return Err(BAD_CHECK.into());    }    let n: u64 = body.parse().unwrap_or(0);    let grouped = n.to_string().as_bytes().rchunks(3).rev().map(|c| std::str::from_utf8(c).unwrap_or("")).collect::<Vec<_>>().join(".");    let mut i = info("RUT (rol único tributario)", format!("{}-{}", grouped, check));    i.holder = Some(if n >= 50_000_000 { "a company" } else { "a person" }.into());    Ok(i)}fn co_nit(s: &str) -> R {    if !(6..=16).contains(&s.len()) || !s.chars().all(|c| c.is_ascii_digit()) {        return Err("a NIT is its digits and a check digit".into());    }    const WS: [u32; 15] = [3, 7, 13, 17, 19, 23, 29, 37, 41, 43, 47, 53, 59, 67, 71];    let (body, check) = s.split_at(s.len() - 1);    let r = body.chars().rev().enumerate().map(|(i, c)| c.to_digit(10).unwrap() * WS[i % 15]).sum::<u32>() % 11;    if (if r < 2 { r } else { 11 - r }) != check.parse::<u32>().unwrap_or(99) {        return Err(BAD_CHECK.into());    }    Ok(info("NIT", format!("{}-{}", body, check)))}fn pe_ruc(s: &str) -> R {    digits(s, &[11])?;    let x = d(s);    let r = 11 - w(&x[..10], &[5, 4, 3, 2, 7, 6, 5, 4, 3, 2]) % 11;    if (if r >= 10 { r - 10 } else { r }) != x[10] {        return Err(BAD_CHECK.into());    }    let mut i = info("RUC", s.to_string());    i.holder = Some(match &s[..2] { "10" => "a person", "15" | "16" | "17" => "a person (special cases)", "20" => "a company", _ => "unknown" }.into());    Ok(i)}fn mx_rfc(s: &str) -> R {    let c: Vec<char> = s.chars().collect();    let letters = match c.len() { 12 => 3, 13 => 4, _ => return Err("an RFC has 12 characters (a company) or 13 (a person)".into()) };    if !c[..letters].iter().all(|x| x.is_ascii_alphabetic() || *x == '&' || *x == 'Ñ') || !c[letters..letters + 6].iter().all(|x| x.is_ascii_digit()) || !c[letters + 6..].iter().all(|x| x.is_ascii_alphanumeric()) {        return Err("an RFC is 3 or 4 letters, a date (YYMMDD) and a 3-character homoclave".into());    }    let date: String = c[letters..letters + 6].iter().collect();    let (m, dd) = (date[2..4].parse::<u32>().unwrap_or(0), date[4..6].parse::<u32>().unwrap_or(0));    if !(1..=12).contains(&m) || !(1..=31).contains(&dd) {        return Err(format!("{} is not a date (YYMMDD)", date));    }    let mut i = info("RFC", s.to_string());    i.holder = Some(if letters == 4 { "a person" } else { "a company" }.into());    i.note = Some(format!("{} on {}", if letters == 4 { "born" } else { "founded" }, date));    Ok(i)}fn ca_bn(s: &str) -> R {    let (bn, program) = (s.get(..9).unwrap_or(s), s.get(9..).unwrap_or(""));    digits(bn, &[9])?;    if !program.is_empty() && !(program.len() == 6 && program[..2].chars().all(|c| c.is_ascii_alphabetic()) && program[2..].chars().all(|c| c.is_ascii_digit())) {        return Err("the program account is 2 letters and 4 digits (RT0001)".into());    }    if !luhn(bn) {        return Err(BAD_CHECK.into());    }    let mut i = info("BN (business number)", if program.is_empty() { bn.to_string() } else { format!("{} {}", bn, program) });    if !program.is_empty() {        i.note = Some(match &program[..2] { "RT" => "a GST/HST account", "RP" => "a payroll account", "RC" => "a corporate income tax account", "RM" => "an import-export account", _ => "a program account" }.into());    }    Ok(i)}fn jp_corp(s: &str) -> R {    digits(s, &[13])?;    let x = d(s);    let sum: u32 = x[1..].iter().rev().enumerate().map(|(i, v)| v * if i % 2 == 0 { 1 } else { 2 }).sum();    if 9 - sum % 9 != x[0] {        return Err(BAD_CHECK.into());    }    Ok(info("Corporate number (法人番号)", s.to_string()))}fn kr_brn(s: &str) -> R {    digits(s, &[10])?;    let x = d(s);    let t = w(&x[..9], &[1, 3, 7, 1, 3, 7, 1, 3, 5]) + x[8] * 5 / 10;    if (10 - t % 10) % 10 != x[9] {        return Err(BAD_CHECK.into());    }    let kind = s[3..5].parse::<u32>().unwrap_or(0);    let mut i = info("Business registration number (사업자등록번호)", format!("{}-{}-{}", &s[..3], &s[3..5], &s[5..]));    i.holder = Some(match kind { 1..=79 => "a sole proprietor", 80 => "a non-profit", 81..=88 => "a corporation", 89 => "a religious group", 90..=99 => "a sole proprietor (tax exempt)", _ => "unknown" }.into());    Ok(i)}fn cn_uscc(s: &str) -> R {    const AL: &str = "0123456789ABCDEFGHJKLMNPQRTUWXY";    if s.len() != 18 || !s.chars().all(|c| AL.contains(c)) {        return Err("a unified social credit code has 18 characters (digits and letters but I, O, S, V, Z)".into());    }    const WS: [usize; 17] = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];    let sum: usize = s.chars().take(17).zip(WS).map(|(c, wt)| AL.find(c).unwrap_or(0) * wt).sum();    if AL.chars().nth((31 - sum % 31) % 31) != s.chars().nth(17) {        return Err(BAD_CHECK.into());    }    let mut i = info("Unified social credit code (统一社会信用代码)", s.to_string());    i.holder = Some(match &s[..1] { "1" => "a government body", "5" => "a social organization", "9" => "a business", "Y" => "a rural collective", _ => "another organization" }.into());    i.note = Some(format!("registered in area {}", &s[2..8]));    Ok(i)}fn tw_ubn(s: &str) -> R {    digits(s, &[8])?;    let x = d(s);    let t: u32 = x.iter().zip([1, 2, 1, 2, 1, 2, 4, 1]).map(|(v, wt)| { let p = v * wt; p / 10 + p % 10 }).sum();    if !(t % 5 == 0 || (x[6] == 7 && (t + 1) % 5 == 0)) {        return Err(BAD_CHECK.into());    }    Ok(info("Unified business number (統一編號)", s.to_string()))}fn th_tin(s: &str) -> R {    digits(s, &[13])?;    let x = d(s);    if (11 - w(&x[..12], &[13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2]) % 11) % 10 != x[12] {        return Err(BAD_CHECK.into());    }    Ok(info("Tax identification number", format!("{}-{}-{}-{}-{}", &s[..1], &s[1..5], &s[5..10], &s[10..12], &s[12..])))}fn ru_inn(s: &str) -> R {    digits(s, &[10, 12])?;    let x = d(s);    let c = |ws: &[u32], n: usize| w(&x[..n], ws) % 11 % 10;    let ok = if s.len() == 10 { c(&[2, 4, 10, 3, 5, 9, 4, 6, 8], 9) == x[9] } else { c(&[7, 2, 4, 10, 3, 5, 9, 4, 6, 8], 10) == x[10] && c(&[3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8], 11) == x[11] };    if !ok {        return Err(BAD_CHECK.into());    }    let mut i = info("INN (ИНН)", s.to_string());    i.holder = Some(if s.len() == 10 { "an organization" } else { "a person or sole trader" }.into());    i.note = Some(format!("registered in region {}", &s[..2]));    Ok(i)}fn sg_uen(s: &str) -> R {    let c: Vec<char> = s.chars().collect();    let (ok, holder) = match c.len() {        9 if c[..8].iter().all(|x| x.is_ascii_digit()) && c[8].is_ascii_alphabetic() => (true, "a business (sole proprietor or partnership)"),        10 if c[..9].iter().all(|x| x.is_ascii_digit()) && c[9].is_ascii_alphabetic() => (true, "a local company"),        10 if "TSR".contains(c[0]) && c[1..3].iter().all(|x| x.is_ascii_digit()) && c[3..5].iter().all(|x| x.is_ascii_alphanumeric()) && c[5..9].iter().all(|x| x.is_ascii_digit()) && c[9].is_ascii_alphabetic() => (true, "another entity (society, charity, LLP...)"),        _ => (false, ""),    };    if !ok {        return Err("a UEN is 8 or 9 digits and a letter, or T/S/R, a year, a type, 4 digits and a letter".into());    }    let mut i = info("UEN (unique entity number)", s.to_string());    i.holder = Some(holder.into());    i.note = Some("the format is checked; ACRA publishes no check letter rule".into());    Ok(i)}const KINDS: &[(&str, &str, &str, fn(&str) -> R)] = &[    ("US", "EIN", "United States: employer identification number", us_ein),    ("AU", "ABN", "Australia: business number", au_abn),    ("AU", "TFN", "Australia: tax file number", au_tfn),    ("NZ", "IRD", "New Zealand: IRD and GST number", nz_ird),    ("IN", "GSTIN", "India: GST identification number", in_gstin),    ("IN", "PAN", "India: permanent account number", in_pan),    ("BR", "CNPJ", "Brazil: company number", br_cnpj),    ("AR", "CUIT", "Argentina: CUIT and CUIL", ar_cuit),    ("CL", "RUT", "Chile: RUT", cl_rut),    ("CO", "NIT", "Colombia: NIT", co_nit),    ("PE", "RUC", "Peru: RUC", pe_ruc),    ("MX", "RFC", "Mexico: RFC", mx_rfc),    ("CA", "BN", "Canada: business number", ca_bn),    ("JP", "CN", "Japan: corporate number", jp_corp),    ("KR", "BRN", "South Korea: business registration number", kr_brn),    ("CN", "USCC", "China: unified social credit code", cn_uscc),    ("TW", "UBN", "Taiwan: unified business number", tw_ubn),    ("TH", "TIN", "Thailand: tax identification number", th_tin),    ("RU", "INN", "Russia: INN", ru_inn),    ("SG", "UEN", "Singapore: unique entity number", sg_uen),];fn result(country: &str, kind: &str, r: R) -> Value {    match r {        Ok(i) => {            let mut m = Map::new();            m.insert("valid".into(), json!(true));            m.insert("country".into(), json!(country));            m.insert("type".into(), json!(kind));            m.insert("name".into(), json!(i.kind));            m.insert("formatted".into(), json!(i.formatted));            m.insert("holder".into(), json!(i.holder));            m.insert("note".into(), json!(i.note));            m.insert("error".into(), Value::Null);            Value::Object(m)        }        Err(e) => json!({ "valid": false, "country": country, "type": kind, "error": e }),    }}/// Checks a tax or business number: `country` (ISO code) and `kind` ("EIN", "ABN", "TFN", "IRD", "GSTIN", "PAN", "CNPJ", "CUIT",/// "RUT", "NIT", "RUC", "RFC", "BN", "CN", "BRN", "USCC", "UBN", "TIN", "INN", "UEN"; "" for the country's first). Gives/// `{ valid, country, type, name, formatted, holder, note, error }`; `holder` says what kind of taxpayer the number marks, where it/// does. Spaces, dots, dashes and slashes are ignored. EU VAT numbers are the `vat` plugin's.pub fn tax_id_check(country: &str, kind: &str, number: &str) -> Value {    let c = country.trim().to_uppercase();    let k = kind.trim().to_uppercase();    let s = clean(number);    match KINDS.iter().find(|(cc, kk, _, _)| *cc == c && (k.is_empty() || *kk == k)) {        Some((cc, kk, _, f)) => result(cc, kk, f(&s)),        None => json!({ "valid": false, "country": c, "type": k, "error": format!("no rules for {} {}: {}", c, k, KINDS.iter().map(|x| format!("{} {}", x.0, x.1)).collect::<Vec<_>>().join(", ")) }),    }}/// Every kind of number `number` could validly be, for a form that does not ask the country: `[{ country, type, name, formatted }]`.pub fn tax_id_guess(number: &str) -> Vec<Value> {    let s = clean(number);    KINDS.iter().filter_map(|(c, k, _, f)| f(&s).ok().map(|i| json!({ "country": c, "type": k, "name": i.kind, "formatted": i.formatted }))).collect()}/// The numbers with rules: `[{ country, type, name }]`.pub fn tax_id_kinds() -> Vec<Value> {    KINDS.iter().map(|(c, k, n, _)| json!({ "country": c, "type": k, "name": n })).collect()}
VAT numbers vat.qwer · 1.0.0EU 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.invoicestaxformsb2bvalidation8 views · 9 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.b2bintegrationinvoicesshipping8 views · 10 downloadsX12 EDI x12.qwer · 1.0.0ANSI X12 EDI for trading with retailers and carriers: interchanges parsed and checked, 850 orders, 810 invoices and 856 ship notices read into plain fields, interchanges built with their envelopes, and 997 acknowledgments.b2bintegrationinvoicesshipping8 views · 6 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.paymentsformscheckoutvalidation7 views · 8 downloadsCheck digits check_digits.qwer · 1.0.0Check digits made and checked: ISBN-10 and ISBN-13 with conversion, EAN-13, EAN-8 and UPC-A barcodes, IMEI, Luhn, ISO 7064 mod 97-10 references and Verhoeff.validationbarcodesbooksimport6 views · 7 downloadsISBN isbn.qwer · 1.0.0ISBNs checked, converted between 10 and 13 digits, hyphenated by the ISBN Agency's ranges into group, publisher and title parts with the group's language or country named, and every ISBN in a text found.booksvalidationcodesebooks6 views · 6 downloads
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

On Tax and business numbers: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.