From dd9934734cb45bc04be54caf7eece021bf38a057 Mon Sep 17 00:00:00 2001 From: Theodoros Plessas <47861171+tplessas@users.noreply.github.com> Date: Tue, 24 Nov 2020 05:16:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(isTaxID):=20new=20validator=20=F0=9F=8E=89?= =?UTF-8?q?=20(#1446)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(isTaxID): Added de-AT locale * Added TIN validation for Austrian numbers * Added source of validation algorithms to header * test(isTaxID): Added unit tests for de-AT TINs * Refactored TIN unit tests to support more locales * Added unit tests for de-AT TINs * Added comments to isTaxID.js to explain behaviour of deAtCheck(tin) * feat(isTaxID): Added el-GR locale * Added TIN validation for Greek numbers * Added relevant tests * feat(isTaxID): Added en-GB locale * Added TIN validation for UK numbers * Added relevant tests * fix(isTaxID): Sanitize TINs before validity testing * Certain EU TINs might be entered with special characters which should not affect their validity/be omitted according to the specification. Such TINs are now sanitized before running checks in isTaxID(str, locale). * Removed en-GB validity check function (not needed, case already covered in isTaxID(str, locale). * Updated/simplified structure of regexes * Updated de-AT tests as some previously invalid TINs are now considered valid in line with the specification. * feat(isTaxID): Added fr/nl-BE locales * Added TIN validation for Belgian numbers * Added relevant tests * Added local TIN names and validation scope (person/entity) to comments * refactor(isTaxID): Added locale aliases * Countries with more than one locale should now have only one entry in the taxIdFormat and taxIdCheck objects, all others added as aliases below the objects. This should help avoid repetition. * Refactored nl-BE as alias to fr-BE * Renamed frNlBeCheck to frNlCheck to reflect changes. * feat(isTaxID): Added fr-FR locale * Added validation for French TINs * Added relevant tests * feat(isTaxID): Added el-CY locale * Added validation for Cypriot TINs * Added relevant tests * Added tests for previously uncovered cases (calling isTaxID without a locale, frBeCheck invalid checksum) * feat(isTaxID): Added hu-HU locale * Added validation for Hungarian TINs * Added relevant tests * Refactored return statements to be one-liners where possible * refactor(isTaxID): Prepare to support more sanitization regexes Different locales might have specific needs wrt acceptable symbols in TINs- in some all are omitted during validation, while others only allow to skip a subset. A new object `sanitizeRegexes` has replaced the previous array, where locale-specific skippable symbol classes are to be placed. When all symbols can be omitted the new variable `allsymbols` is referenced. Aliases have also been added for the new object in line with the others and the isTaxID function checks for the locale's inclusion in `sanitizeRegexes`. * feat(isTaxID): Add de-DE locale * Add TIN validation for German numbers * Add relevant tests * feat(isTaxID): Add hr-HR locale * Moved de-DE check digit calculation routine to new function `iso7064Check()` to be used for other conforming locales. * Add TIN validation for Croatian numbers * Add relevant tests * Refactor deDeCheck() to use iso7064Check() * feat(isTaxID): Add bg-BG locale * Add TIN validation for Bulgarian numbers * Add relevant tests * feat(isTaxID): Add cs-CZ locale * Add TIN validation for Czech numbers * Add relevant tests * fix(isTaxID): Add el-GR first digit validation * Add validation for first digit of Greek TINs * Refactor el-GR tests * Add info for testable en-GB TINs * feat(isTaxID): Add sk-SK locale * Add TIN validation for Slovakian numbers * Add relevant tests * feat(isTaxID): Add dk-DK locale * Add TIN validation for Danish numbers * Add relevant tests * feat(isTaxID): Add et-EE locale * Add TIN validation for Estonian numbers * Add relevant tests * feat(isTaxID): Add lt-LT locale * Add TIN validation for Lithuanian numbers (as alias of et-EE) * Add relevant tests * feat(isTaxID): Add fi-FI locale * Add TIN validation for Finnish numbers * Add relevant tests * feat(isTaxID): Add it-IT locale * Add TIN validation for Italian numbers * Add relevant tests * feat(isTaxID): Add en-IE locale * Add TIN validation for Irish numbers * Add relevant tests * feat(isTaxID): Add lv-LV locale * Add TIN validation for Latvian numbers * Add relevant tests * refactor(isTaxID): Remove unneeded parseInt() calls * Remove parseInt() calls in year extraction procedures of functions to improve performance and readability * refactor(isTaxID): Add Luhn validation function * Add luhnCheck() to be used by conforming locale TINs * Refactor deAtCheck() to use luhnCheck() * refactor(isTaxID): Add reverse multiplication function * Add reverseMultiplyAndSum() to support new locale check functions * feat(isTaxID): Add sv-SE locale * Add TIN validation for Swedish numbers * Add relevant tests * feat(isTaxID): Add nl-NL locale * Add TIN validation for Dutch numbers * Add relevant tests * Refactor enIeCheck() to use reverseMultiplyAndSum() * feat(isTaxID): Add pt-PT locale * Add TIN validation for Portugese numbers * Add relevant tests * feat(isTaxID): Add sl-SI locale * Add TIN validation for Slovenian numbers * Add relevant tests * feat(isTaxID): Add es-ES locale * Add TIN validation for Spanish numbers * Add relevant tests * feat(isTaxID): Add ro-RO locale * Add TIN validation for Romanian numbers * Add relevant tests * feat(isTaxID): Add mt-MT locale * Add TIN validation for Maltese numbers * Add relevant tests * feat(isTaxID): Add pl-PL locale * Add TIN validation for Polish numbers * Add relevant tests * refactor(isTaxID): Add any case support * Add support for both uppercase and lowercase letters where not specifically defined in the DG TAXUD document * chore(isTaxID): Add Verhoeff validation function * Add verhoeffCheck() to be used by future locale TINs * feat(isTaxID): Add fr/lb-LU locale * Add TIN validation for Luxembourgish numbers * Add relevant tests * docs(README): Update isTaxID() description * Add supported locale list and info message to isTaxID() description * remove(isTaxID): Remove codice catastale validation * Remove codice catastale validation subroutine from it-IT (to be included in another upstream PR) for further review. * refactor(isTaxID): Move helper validation algorithms * General-purpose validation algorithms have benn moved to `src/lib/util/algoritms.js` to support further use. * Validation algorithms have been refactored to use strings as input * isTaxID has been refactored to use `algorithms.js` --- README.md | 2 +- src/lib/isTaxID.js | 1045 +++++++++++++++++++++++++++++++++++- src/lib/util/algorithms.js | 97 ++++ test/validators.js | 447 ++++++++++++++- 4 files changed, 1584 insertions(+), 7 deletions(-) create mode 100644 src/lib/util/algorithms.js diff --git a/README.md b/README.md index fb5fcdc22..e50b5126e 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ Validator | Description **isUppercase(str)** | check if the string is uppercase. **isSlug** | Check if the string is of type slug. `Options` allow a single hyphen between string. e.g. [`cn-cn`, `cn-c-c`] **isStrongPassword(str [, options])** | Check if a password is strong or not. Allows for custom requirements or scoring rules. If `returnScore` is true, then the function returns an integer score for the password rather than a boolean.
Default options:
`{ minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1, returnScore: false, pointsPerUnique: 1, pointsPerRepeat: 0.5, pointsForContainingLower: 10, pointsForContainingUpper: 10, pointsForContainingNumber: 10, pointsForContainingSymbol: 10 }` -**isTaxID(str, locale)** | Check if the given value is a valid Tax Identification Number. Default locale is `en-US` +**isTaxID(str, locale)** | Check if the given value is a valid Tax Identification Number. Default locale is `en-US`.

More info about exact TIN support can be found in `src/lib/isTaxID.js`

Supported locales: `[ 'bg-BG', 'cs-CZ', 'de-AT', 'de-DE', 'dk-DK', 'el-CY', 'el-GR', 'en-GB', 'en-IE', 'en-US', 'es-ES', 'et-EE', 'fi-FI', 'fr-BE', 'fr-FR', 'fr-LU', 'hr-HR', 'hu-HU', 'it-IT', 'lb-LU', 'lt-LT', 'lv-LV' 'mt-MT', 'nl-BE', 'nl-NL', 'pl-PL', 'pt-PT', 'ro-RO', 'sk-SK', 'sl-SI', 'sv-SE' ]` **isURL(str [, options])** | check if the string is an URL.

`options` is an object which defaults to `{ protocols: ['http','https','ftp'], require_tld: true, require_protocol: false, require_host: true, require_valid_protocol: true, allow_underscores: false, host_whitelist: false, host_blacklist: false, allow_trailing_dot: false, allow_protocol_relative_urls: false, disallow_auth: false }`.

require_protocol - if set as true isURL will return false if protocol is not present in the URL.
require_valid_protocol - isURL will check if the URL's protocol is present in the protocols option.
protocols - valid protocols can be modified with this option.
require_host - if set as false isURL will not check if host is present in the URL.
require_port - if set as true isURL will check if port is present in the URL.
allow_protocol_relative_urls - if set as true protocol relative URLs will be allowed.
validate_length - if set as false isURL will skip string length validation (2083 characters is IE max URL length). **isUUID(str [, version])** | check if the string is a UUID (version 3, 4 or 5). **isVariableWidth(str)** | check if the string contains a mixture of full and half-width chars. diff --git a/src/lib/isTaxID.js b/src/lib/isTaxID.js index 446ebd088..40edfe381 100644 --- a/src/lib/isTaxID.js +++ b/src/lib/isTaxID.js @@ -1,8 +1,17 @@ import assertString from './util/assertString'; +import * as algorithms from './util/algorithms'; +import isDate from './isDate'; /** - * en-US TIN Validation + * TIN Validation + * Validates Tax Identification Numbers (TINs) from the US, EU member states and the United Kingdom. * + * EU-UK: + * National TIN validity is calculated using public algorithms as made available by DG TAXUD. + * + * See `https://ec.europa.eu/taxation_customs/tin/specs/FS-TIN%20Algorithms-Public.docx` for more information. + * + * US: * An Employer Identification Number (EIN), also known as a Federal Tax Identification Number, * is used to identify a business entity. * @@ -14,6 +23,289 @@ import assertString from './util/assertString'; * for more information. */ +// Locale functions + +/* + * bg-BG validation function + * (Edinen graždanski nomer (EGN/ЕГН), persons only) + * Checks if birth date (first six digits) is valid and calculates check (last) digit + */ +function bgBgCheck(tin) { + // Extract full year, normalize month and check birth date validity + let century_year = tin.slice(0, 2); + let month = parseInt(tin.slice(2, 4), 10); + if (month > 40) { + month -= 40; + century_year = `20${century_year}`; + } else if (month > 20) { + month -= 20; + century_year = `18${century_year}`; + } else { + century_year = `19${century_year}`; + } + if (month < 10) { month = `0${month}`; } + const date = `${century_year}/${month}/${tin.slice(4, 6)}`; + if (!isDate(date, 'YYYY/MM/DD')) { return false; } + + // split digits into an array for further processing + const digits = tin.split('').map(a => parseInt(a, 10)); + + // Calculate checksum by multiplying digits with fixed values + const multip_lookup = [2, 4, 8, 5, 10, 9, 7, 3, 6]; + let checksum = 0; + for (let i = 0; i < multip_lookup.length; i++) { + checksum += digits[i] * multip_lookup[i]; + } + checksum = checksum % 11 === 10 ? 0 : checksum % 11; + return checksum === digits[9]; +} + +/* + * cs-CZ validation function + * (Rodné číslo (RČ), persons only) + * Checks if birth date (first six digits) is valid and divisibility by 11 + * Material not in DG TAXUD document sourced from: + * -`https://lorenc.info/3MA381/overeni-spravnosti-rodneho-cisla.htm` + * -`https://www.mvcr.cz/clanek/rady-a-sluzby-dokumenty-rodne-cislo.aspx` + */ +function csCzCheck(tin) { + tin = tin.replace(/\W/, ''); + + // Extract full year from TIN length + let full_year = parseInt(tin.slice(0, 2), 10); + if (tin.length === 10) { + if (full_year < 54) { + full_year = `20${full_year}`; + } else { + full_year = `19${full_year}`; + } + } else { + if (tin.slice(6) === '000') { return false; } // Three-zero serial not assigned before 1954 + if (full_year < 54) { + full_year = `19${full_year}`; + } else { + return false; // No 18XX years seen in any of the resources + } + } + // Add missing zero if needed + if (full_year.length === 3) { + full_year = [full_year.slice(0, 2), '0', full_year.slice(2)].join(''); + } + + // Extract month from TIN and normalize + let month = parseInt(tin.slice(2, 4), 10); + if (month > 50) { + month -= 50; + } + if (month > 20) { + // Month-plus-twenty was only introduced in 2004 + if (parseInt(full_year, 10) < 2004) { return false; } + month -= 20; + } + if (month < 10) { month = `0${month}`; } + + // Check date validity + const date = `${full_year}/${month}/${tin.slice(4, 6)}`; + if (!isDate(date, 'YYYY/MM/DD')) { return false; } + + // Verify divisibility by 11 + if (tin.length === 10) { + if (parseInt(tin, 10) % 11 !== 0) { + // Some numbers up to and including 1985 are still valid if + // check (last) digit equals 0 and modulo of first 9 digits equals 10 + const checkdigit = parseInt(tin.slice(0, 9), 10) % 11; + if (parseInt(full_year, 10) < 1986 && checkdigit === 10) { + if (parseInt(tin.slice(9), 10) !== 0) { return false; } + } else { + return false; + } + } + } + return true; +} + +/* + * de-AT validation function + * (Abgabenkontonummer, persons/entities) + * Verify TIN validity by calling luhnCheck() + */ +function deAtCheck(tin) { + return algorithms.luhnCheck(tin); +} + +/* + * de-DE validation function + * (Steueridentifikationsnummer (Steuer-IdNr.), persons only) + * Tests for single duplicate/triplicate value, then calculates ISO 7064 check (last) digit + * Partial implementation of spec (same result with both algorithms always) + */ +function deDeCheck(tin) { + // Split digits into an array for further processing + const digits = tin.split('').map(a => parseInt(a, 10)); + + // Fill array with strings of number positions + let occurences = []; + for (let i = 0; i < digits.length - 1; i++) { + occurences.push(''); + for (let j = 0; j < digits.length - 1; j++) { + if (digits[i] === digits[j]) { + occurences[i] += j; + } + } + } + + // Remove digits with one occurence and test for only one duplicate/triplicate + occurences = occurences.filter(a => a.length > 1); + if (occurences.length !== 2 && occurences.length !== 3) { return false; } + + // In case of triplicate value only two digits are allowed next to each other + if (occurences[0].length === 3) { + const trip_locations = occurences[0].split('').map(a => parseInt(a, 10)); + let recurrent = 0; // Amount of neighbour occurences + for (let i = 0; i < trip_locations.length - 1; i++) { + if (trip_locations[i] + 1 === trip_locations[i + 1]) { + recurrent += 1; + } + } + if (recurrent === 2) { + return false; + } + } + return algorithms.iso7064Check(tin); +} + +/* + * dk-DK validation function + * (CPR-nummer (personnummer), persons only) + * Checks if birth date (first six digits) is valid and assigned to century (seventh) digit, + * and calculates check (last) digit + */ +function dkDkCheck(tin) { + tin = tin.replace(/\W/, ''); + + // Extract year, check if valid for given century digit and add century + let year = parseInt(tin.slice(4, 6), 10); + const century_digit = tin.slice(6, 7); + switch (century_digit) { + case '0': + case '1': + case '2': + case '3': + year = `19${year}`; + break; + case '4': + case '9': + if (year < 37) { + year = `20${year}`; + } else { + year = `19${year}`; + } + break; + default: + if (year < 37) { + year = `20${year}`; + } else if (year > 58) { + year = `18${year}`; + } else { + return false; + } + break; + } + // Add missing zero if needed + if (year.length === 3) { + year = [year.slice(0, 2), '0', year.slice(2)].join(''); + } + // Check date validity + const date = `${year}/${tin.slice(2, 4)}/${tin.slice(0, 2)}`; + if (!isDate(date, 'YYYY/MM/DD')) { return false; } + + // Split digits into an array for further processing + const digits = tin.split('').map(a => parseInt(a, 10)); + let checksum = 0; + let weight = 4; + // Multiply by weight and add to checksum + for (let i = 0; i < 9; i++) { + checksum += digits[i] * weight; + weight -= 1; + if (weight === 1) { + weight = 7; + } + } + checksum %= 11; + if (checksum === 1) { return false; } + return checksum === 0 ? digits[9] === 0 : digits[9] === 11 - checksum; +} + +/* + * el-CY validation function + * (Arithmos Forologikou Mitroou (AFM/ΑΦΜ), persons only) + * Verify TIN validity by calculating ASCII value of check (last) character + */ +function elCyCheck(tin) { + // split digits into an array for further processing + const digits = tin.slice(0, 8).split('').map(a => parseInt(a, 10)); + + let checksum = 0; + // add digits in even places + for (let i = 1; i < digits.length; i += 2) { + checksum += digits[i]; + } + + // add digits in odd places + for (let i = 0; i < digits.length; i += 2) { + if (digits[i] < 2) { + checksum += 1 - digits[i]; + } else { + checksum += (2 * (digits[i] - 2)) + 5; + if (digits[i] > 4) { + checksum += 2; + } + } + } + return String.fromCharCode((checksum % 26) + 65) === tin.charAt(8); +} + +/* + * el-GR validation function + * (Arithmos Forologikou Mitroou (AFM/ΑΦΜ), persons/entities) + * Verify TIN validity by calculating check (last) digit + * Algorithm not in DG TAXUD document- sourced from: + * - `http://epixeirisi.gr/%CE%9A%CE%A1%CE%99%CE%A3%CE%99%CE%9C%CE%91-%CE%98%CE%95%CE%9C%CE%91%CE%A4%CE%91-%CE%A6%CE%9F%CE%A1%CE%9F%CE%9B%CE%9F%CE%93%CE%99%CE%91%CE%A3-%CE%9A%CE%91%CE%99-%CE%9B%CE%9F%CE%93%CE%99%CE%A3%CE%A4%CE%99%CE%9A%CE%97%CE%A3/23791/%CE%91%CF%81%CE%B9%CE%B8%CE%BC%CF%8C%CF%82-%CE%A6%CE%BF%CF%81%CE%BF%CE%BB%CE%BF%CE%B3%CE%B9%CE%BA%CE%BF%CF%8D-%CE%9C%CE%B7%CF%84%CF%81%CF%8E%CE%BF%CF%85` + */ +function elGrCheck(tin) { + // split digits into an array for further processing + const digits = tin.split('').map(a => parseInt(a, 10)); + + let checksum = 0; + for (let i = 0; i < 8; i++) { + checksum += digits[i] * (2 ** (8 - i)); + } + return checksum % 11 === digits[8]; +} + +/* + * en-GB validation function (should go here if needed) + * (National Insurance Number (NINO) or Unique Taxpayer Reference (UTR), + * persons/entities respectively) + */ + +/* + * en-IE validation function + * (Personal Public Service Number (PPS No), persons only) + * Verify TIN validity by calculating check (second to last) character + */ +function enIeCheck(tin) { + let checksum = algorithms.reverseMultiplyAndSum(tin.split('').slice(0, 7).map(a => parseInt(a, 10)), 8); + if (tin.length === 9 && tin[8] !== 'W') { + checksum += (tin[8].charCodeAt(0) - 64) * 9; + } + + checksum %= 23; + if (checksum === 0) { + return tin[7].toUpperCase() === 'W'; + } + return tin[7].toUpperCase() === String.fromCharCode(64 + checksum); +} // Valid US IRS campus prefixes const enUsCampusPrefix = { @@ -54,20 +346,757 @@ function enUsCheck(tin) { return enUsGetPrefixes().indexOf(tin.substr(0, 2)) !== -1; } -// tax id regex formats for various locales +/* + * es-ES validation function + * (Documento Nacional de Identidad (DNI) + * or Número de Identificación de Extranjero (NIE), persons only) + * Verify TIN validity by calculating check (last) character + */ +function esEsCheck(tin) { + // Split characters into an array for further processing + let chars = tin.toUpperCase().split(''); + + // Replace initial letter if needed + if (isNaN(parseInt(chars[0], 10)) && chars.length > 1) { + let lead_replace = 0; + switch (chars[0]) { + case 'Y': + lead_replace = 1; + break; + case 'Z': + lead_replace = 2; + break; + default: + } + chars.splice(0, 1, lead_replace); + // Fill with zeros if smaller than proper + } else { + while (chars.length < 9) { + chars.unshift(0); + } + } + + // Calculate checksum and check according to lookup + const lookup = ['T', 'R', 'W', 'A', 'G', 'M', 'Y', 'F', 'P', 'D', 'X', 'B', 'N', 'J', 'Z', 'S', 'Q', 'V', 'H', 'L', 'C', 'K', 'E']; + chars = chars.join(''); + let checksum = (parseInt(chars.slice(0, 8), 10) % 23); + return chars[8] === lookup[checksum]; +} + +/* + * et-EE validation function + * (Isikukood (IK), persons only) + * Checks if birth date (century digit and six following) is valid and calculates check (last) digit + * Material not in DG TAXUD document sourced from: + * - `https://www.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/Estonia-TIN.pdf` + */ +function etEeCheck(tin) { + // Extract year and add century + let full_year = tin.slice(1, 3); + const century_digit = tin.slice(0, 1); + switch (century_digit) { + case '1': + case '2': + full_year = `18${full_year}`; + break; + case '3': + case '4': + full_year = `19${full_year}`; + break; + default: + full_year = `20${full_year}`; + break; + } + // Check date validity + const date = `${full_year}/${tin.slice(3, 5)}/${tin.slice(5, 7)}`; + if (!isDate(date, 'YYYY/MM/DD')) { return false; } + + // Split digits into an array for further processing + const digits = tin.split('').map(a => parseInt(a, 10)); + let checksum = 0; + let weight = 1; + // Multiply by weight and add to checksum + for (let i = 0; i < 10; i++) { + checksum += digits[i] * weight; + weight += 1; + if (weight === 10) { + weight = 1; + } + } + // Do again if modulo 11 of checksum is 10 + if (checksum % 11 === 10) { + checksum = 0; + weight = 3; + for (let i = 0; i < 10; i++) { + checksum += digits[i] * weight; + weight += 1; + if (weight === 10) { + weight = 1; + } + } + if (checksum % 11 === 10) { return digits[10] === 0; } + } + + return checksum % 11 === digits[10]; +} + +/* + * fi-FI validation function + * (Henkilötunnus (HETU), persons only) + * Checks if birth date (first six digits plus century symbol) is valid + * and calculates check (last) digit + */ +function fiFiCheck(tin) { + // Extract year and add century + let full_year = tin.slice(4, 6); + const century_symbol = tin.slice(6, 7); + switch (century_symbol) { + case '+': + full_year = `18${full_year}`; + break; + case '-': + full_year = `19${full_year}`; + break; + default: + full_year = `20${full_year}`; + break; + } + // Check date validity + const date = `${full_year}/${tin.slice(2, 4)}/${tin.slice(0, 2)}`; + if (!isDate(date, 'YYYY/MM/DD')) { return false; } + + // Calculate check character + let checksum = parseInt((tin.slice(0, 6) + tin.slice(7, 10)), 10) % 31; + if (checksum < 10) { return checksum === parseInt(tin.slice(10), 10); } + + checksum -= 10; + const letters_lookup = ['A', 'B', 'C', 'D', 'E', 'F', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y']; + return letters_lookup[checksum] === tin.slice(10); +} + +/* + * fr/nl-BE validation function + * (Numéro national (N.N.), persons only) + * Checks if birth date (first six digits) is valid and calculates check (last two) digits + */ +function frBeCheck(tin) { + // Zero month/day value is acceptable + if (tin.slice(2, 4) !== '00' || tin.slice(4, 6) !== '00') { + // Extract date from first six digits of TIN + const date = `${tin.slice(0, 2)}/${tin.slice(2, 4)}/${tin.slice(4, 6)}`; + if (!isDate(date, 'YY/MM/DD')) { return false; } + } + + let checksum = 97 - (parseInt(tin.slice(0, 9), 10) % 97); + const checkdigits = parseInt(tin.slice(9, 11), 10); + if (checksum !== checkdigits) { + checksum = 97 - (parseInt(`2${tin.slice(0, 9)}`, 10) % 97); + if (checksum !== checkdigits) { + return false; + } + } + return true; +} + +/* + * fr-FR validation function + * (Numéro fiscal de référence (numéro SPI), persons only) + * Verify TIN validity by calculating check (last three) digits + */ +function frFrCheck(tin) { + tin = tin.replace(/\s/g, ''); + const checksum = parseInt(tin.slice(0, 10), 10) % 511; + const checkdigits = parseInt(tin.slice(10, 13), 10); + return checksum === checkdigits; +} + +/* + * fr/lb-LU validation function + * (numéro d’identification personnelle, persons only) + * Verify birth date validity and run Luhn and Verhoeff checks + */ +function frLuCheck(tin) { + // Extract date and check validity + const date = `${tin.slice(0, 4)}/${tin.slice(4, 6)}/${tin.slice(6, 8)}`; + if (!isDate(date, 'YYYY/MM/DD')) { return false; } + + // Run Luhn check + if (!algorithms.luhnCheck(tin.slice(0, 12))) { return false; } + // Remove Luhn check digit and run Verhoeff check + return algorithms.verhoeffCheck(`${tin.slice(0, 11)}${tin[12]}`); +} + +/* + * hr-HR validation function + * (Osobni identifikacijski broj (OIB), persons/entities) + * Verify TIN validity by calling iso7064Check(digits) + */ +function hrHrCheck(tin) { + return algorithms.iso7064Check(tin); +} + +/* + * hu-HU validation function + * (Adóazonosító jel, persons only) + * Verify TIN validity by calculating check (last) digit + */ +function huHuCheck(tin) { + // split digits into an array for further processing + const digits = tin.split('').map(a => parseInt(a, 10)); + + let checksum = 8; + for (let i = 1; i < 9; i++) { + checksum += digits[i] * (i + 1); + } + return checksum % 11 === digits[9]; +} + +/* + * lt-LT validation function (should go here if needed) + * (Asmens kodas, persons/entities respectively) + * Current validation check is alias of etEeCheck- same format applies + */ + +/* + * it-IT first/last name validity check + * Accepts it-IT TIN-encoded names as a three-element character array and checks their validity + * Due to lack of clarity between resources ("Are only Italian consonants used? + * What happens if a person has X in their name?" etc.) only two test conditions + * have been implemented: + * Vowels may only be followed by other vowels or an X character + * and X characters after vowels may only be followed by other X characters. + */ +function itItNameCheck(name) { + // true at the first occurence of a vowel + let vowelflag = false; + + // true at the first occurence of an X AFTER vowel + // (to properly handle last names with X as consonant) + let xflag = false; + + for (let i = 0; i < 3; i++) { + if (!vowelflag && /[AEIOU]/.test(name[i])) { + vowelflag = true; + } else if (!xflag && vowelflag && (name[i] === 'X')) { + xflag = true; + } else if (i > 0) { + if (vowelflag && !xflag) { + if (!/[AEIOU]/.test(name[i])) { return false; } + } + if (xflag) { + if (!/X/.test(name[i])) { return false; } + } + } + } + return true; +} + +/* + * it-IT validation function + * (Codice fiscale (TIN-IT), persons only) + * Verify name, birth date and codice catastale validity + * and calculate check character. + * Material not in DG-TAXUD document sourced from: + * `https://en.wikipedia.org/wiki/Italian_fiscal_code` + */ +function itItCheck(tin) { + // Capitalize and split characters into an array for further processing + const chars = tin.toUpperCase().split(''); + + // Check first and last name validity calling itItNameCheck() + if (!itItNameCheck(chars.slice(0, 3))) { return false; } + if (!itItNameCheck(chars.slice(3, 6))) { return false; } + + // Convert letters in number spaces back to numbers if any + const number_locations = [6, 7, 9, 10, 12, 13, 14]; + const number_replace = { + L: '0', + M: '1', + N: '2', + P: '3', + Q: '4', + R: '5', + S: '6', + T: '7', + U: '8', + V: '9', + }; + for (const i of number_locations) { + if (chars[i] in number_replace) { + chars.splice(i, 1, number_replace[chars[i]]); + } + } + + // Extract month and day, and check date validity + const month_replace = { + A: '01', + B: '02', + C: '03', + D: '04', + E: '05', + H: '06', + L: '07', + M: '08', + P: '09', + R: '10', + S: '11', + T: '12', + }; + let month = month_replace[chars[8]]; + + let day = parseInt(chars[9] + chars[10], 10); + if (day > 40) { day -= 40; } + if (day < 10) { day = `0${day}`; } + + const date = `${chars[6]}${chars[7]}/${month}/${day}`; + if (!isDate(date, 'YY/MM/DD')) { return false; } + + // Calculate check character by adding up even and odd characters as numbers + let checksum = 0; + for (let i = 1; i < chars.length - 1; i += 2) { + let char_to_int = parseInt(chars[i], 10); + if (isNaN(char_to_int)) { + char_to_int = chars[i].charCodeAt(0) - 65; + } + checksum += char_to_int; + } + + const odd_convert = { // Maps of characters at odd places + A: 1, + B: 0, + C: 5, + D: 7, + E: 9, + F: 13, + G: 15, + H: 17, + I: 19, + J: 21, + K: 2, + L: 4, + M: 18, + N: 20, + O: 11, + P: 3, + Q: 6, + R: 8, + S: 12, + T: 14, + U: 16, + V: 10, + W: 22, + X: 25, + Y: 24, + Z: 23, + 0: 1, + 1: 0, + }; + for (let i = 0; i < chars.length - 1; i += 2) { + let char_to_int = 0; + if (chars[i] in odd_convert) { + char_to_int = odd_convert[chars[i]]; + } else { + let multiplier = parseInt(chars[i], 10); + char_to_int = (2 * multiplier) + 1; + if (multiplier > 4) { + char_to_int += 2; + } + } + checksum += char_to_int; + } + + if (String.fromCharCode(65 + (checksum % 26)) !== chars[15]) { return false; } + return true; +} + +/* + * lv-LV validation function + * (Personas kods (PK), persons only) + * Check validity of birth date and calculate check (last) digit + * Support only for old format numbers (not starting with '32', issued before 2017/07/01) + * Material not in DG TAXUD document sourced from: + * `https://boot.ritakafija.lv/forums/index.php?/topic/88314-personas-koda-algoritms-%C4%8Deksumma/` + */ +function lvLvCheck(tin) { + tin = tin.replace(/\W/, ''); + // Extract date from TIN + const day = tin.slice(0, 2); + if (day !== '32') { // No date/checksum check if new format + const month = tin.slice(2, 4); + if (month !== '00') { // No date check if unknown month + let full_year = tin.slice(4, 6); + switch (tin[6]) { + case '0': + full_year = `18${full_year}`; + break; + case '1': + full_year = `19${full_year}`; + break; + default: + full_year = `20${full_year}`; + break; + } + // Check date validity + const date = `${full_year}/${tin.slice(2, 4)}/${day}`; + if (!isDate(date, 'YYYY/MM/DD')) { return false; } + } + + // Calculate check digit + let checksum = 1101; + const multip_lookup = [1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; + for (let i = 0; i < tin.length - 1; i++) { + checksum -= parseInt(tin[i], 10) * multip_lookup[i]; + } + return (parseInt(tin[10], 10) === checksum % 11); + } + return true; +} + +/* + * mt-MT validation function + * (Identity Card Number or Unique Taxpayer Reference, persons/entities) + * Verify Identity Card Number structure (no other tests found) + */ +function mtMtCheck(tin) { + if (tin.length !== 9) { // No tests for UTR + let chars = tin.toUpperCase().split(''); + // Fill with zeros if smaller than proper + while (chars.length < 8) { + chars.unshift(0); + } + // Validate format according to last character + switch (tin[7]) { + case 'A': + case 'P': + if (parseInt(chars[6], 10) === 0) { return false; } + break; + default: { + const first_part = parseInt(chars.join('').slice(0, 5), 10); + if (first_part > 32000) { return false; } + const second_part = parseInt(chars.join('').slice(5, 7), 10); + if (first_part === second_part) { return false; } + } + } + } + return true; +} + +/* + * nl-NL validation function + * (Burgerservicenummer (BSN) or Rechtspersonen Samenwerkingsverbanden Informatie Nummer (RSIN), + * persons/entities respectively) + * Verify TIN validity by calculating check (last) digit (variant of MOD 11) + */ +function nlNlCheck(tin) { + return algorithms.reverseMultiplyAndSum(tin.split('').slice(0, 8).map(a => parseInt(a, 10)), 9) % 11 === parseInt(tin[8], 10); +} + +/* + * pl-PL validation function + * (Powszechny Elektroniczny System Ewidencji Ludności (PESEL) + * or Numer identyfikacji podatkowej (NIP), persons/entities) + * Verify TIN validity by validating birth date (PESEL) and calculating check (last) digit + */ +function plPlCheck(tin) { + // NIP + if (tin.length === 10) { + // Calculate last digit by multiplying with lookup + const lookup = [6, 5, 7, 2, 3, 4, 5, 6, 7]; + let checksum = 0; + for (let i = 0; i < lookup.length; i++) { + checksum += parseInt(tin[i], 10) * lookup[i]; + } + checksum %= 11; + if (checksum === 10) { return false; } + return (checksum === parseInt(tin[9], 10)); + } + + // PESEL + // Extract full year using month + let full_year = tin.slice(0, 2); + let month = parseInt(tin.slice(2, 4), 10); + if (month > 80) { + full_year = `18${full_year}`; + month -= 80; + } else if (month > 60) { + full_year = `22${full_year}`; + month -= 60; + } else if (month > 40) { + full_year = `21${full_year}`; + month -= 40; + } else if (month > 20) { + full_year = `20${full_year}`; + month -= 20; + } else { + full_year = `19${full_year}`; + } + // Add leading zero to month if needed + if (month < 10) { month = `0${month}`; } + // Check date validity + const date = `${full_year}/${month}/${tin.slice(4, 6)}`; + if (!isDate(date, 'YYYY/MM/DD')) { return false; } + + // Calculate last digit by mulitplying with odd one-digit numbers except 5 + let checksum = 0; + let multiplier = 1; + for (let i = 0; i < tin.length - 1; i++) { + checksum += (parseInt(tin[i], 10) * multiplier) % 10; + multiplier += 2; + if (multiplier > 10) { + multiplier = 1; + } else if (multiplier === 5) { + multiplier += 2; + } + } + checksum = 10 - (checksum % 10); + return checksum === parseInt(tin[10], 10); +} + +/* + * pt-PT validation function + * (Número de identificação fiscal (NIF), persons/entities) + * Verify TIN validity by calculating check (last) digit (variant of MOD 11) + */ +function ptPtCheck(tin) { + let checksum = 11 - (algorithms.reverseMultiplyAndSum(tin.split('').slice(0, 8).map(a => parseInt(a, 10)), 9) % 11); + if (checksum > 9) { return parseInt(tin[8], 10) === 0; } + return checksum === parseInt(tin[8], 10); +} + +/* + * ro-RO validation function + * (Cod Numeric Personal (CNP) or Cod de înregistrare fiscală (CIF), + * persons only) + * Verify CNP validity by calculating check (last) digit (test not found for CIF) + * Material not in DG TAXUD document sourced from: + * `https://en.wikipedia.org/wiki/National_identification_number#Romania` + */ +function roRoCheck(tin) { + if (tin.slice(0, 4) !== '9000') { // No test found for this format + // Extract full year using century digit if possible + let full_year = tin.slice(1, 3); + switch (tin[0]) { + case '1': + case '2': + full_year = `19${full_year}`; + break; + case '3': + case '4': + full_year = `18${full_year}`; + break; + case '5': + case '6': + full_year = `20${full_year}`; + break; + default: + } + + // Check date validity + const date = `${full_year}/${tin.slice(3, 5)}/${tin.slice(5, 7)}`; + if (date.length === 8) { + if (!isDate(date, 'YY/MM/DD')) { return false; } + } else if (!isDate(date, 'YYYY/MM/DD')) { return false; } + + // Calculate check digit + const digits = tin.split('').map(a => parseInt(a, 10)); + const multipliers = [2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9]; + let checksum = 0; + for (let i = 0; i < multipliers.length; i++) { + checksum += digits[i] * multipliers[i]; + } + if (checksum % 11 === 10) { return digits[12] === 1; } + return digits[12] === checksum % 11; + } + return true; +} + +/* + * sk-SK validation function + * (Rodné číslo (RČ) or bezvýznamové identifikačné číslo (BIČ), persons only) + * Checks validity of pre-1954 birth numbers (rodné číslo) only + * Due to the introduction of the pseudo-random BIČ it is not possible to test + * post-1954 birth numbers without knowing whether they are BIČ or RČ beforehand + */ +function skSkCheck(tin) { + if (tin.length === 9) { + tin = tin.replace(/\W/, ''); + if (tin.slice(6) === '000') { return false; } // Three-zero serial not assigned before 1954 + + // Extract full year from TIN length + let full_year = parseInt(tin.slice(0, 2), 10); + if (full_year > 53) { return false; } + if (full_year < 10) { + full_year = `190${full_year}`; + } else { + full_year = `19${full_year}`; + } + + // Extract month from TIN and normalize + let month = parseInt(tin.slice(2, 4), 10); + if (month > 50) { + month -= 50; + } + if (month < 10) { month = `0${month}`; } + + // Check date validity + const date = `${full_year}/${month}/${tin.slice(4, 6)}`; + if (!isDate(date, 'YYYY/MM/DD')) { return false; } + } + return true; +} + +/* + * sl-SI validation function + * (Davčna številka, persons/entities) + * Verify TIN validity by calculating check (last) digit (variant of MOD 11) + */ +function slSiCheck(tin) { + let checksum = 11 - (algorithms.reverseMultiplyAndSum(tin.split('').slice(0, 7).map(a => parseInt(a, 10)), 8) % 11); + if (checksum === 10) { return parseInt(tin[7], 10) === 0; } + return checksum === parseInt(tin[7], 10); +} + +/* + * sv-SE validation function + * (Personnummer or samordningsnummer, persons only) + * Checks validity of birth date and calls luhnCheck() to validate check (last) digit + */ +function svSeCheck(tin) { + // Make copy of TIN and normalize to two-digit year form + let tin_copy = tin.slice(0); + if (tin.length > 11) { + tin_copy = tin_copy.slice(2); + } + + // Extract date of birth + let full_year = ''; + const month = tin_copy.slice(2, 4); + let day = parseInt(tin_copy.slice(4, 6), 10); + if (tin.length > 11) { + full_year = tin.slice(0, 4); + } else { + full_year = tin.slice(0, 2); + if (tin.length === 11 && day < 60) { + // Extract full year from centenarian symbol + // Should work just fine until year 10000 or so + let current_year = new Date().getFullYear().toString(); + const current_century = parseInt(current_year.slice(0, 2), 10); + current_year = parseInt(current_year, 10); + if (tin[6] === '-') { + if (parseInt(`${current_century}${full_year}`, 10) > current_year) { + full_year = `${current_century - 1}${full_year}`; + } else { + full_year = `${current_century}${full_year}`; + } + } else { + full_year = `${current_century - 1}${full_year}`; + if (current_year - parseInt(full_year, 10) < 100) { return false; } + } + } + } + + // Normalize day and check date validity + if (day > 60) { day -= 60; } + if (day < 10) { day = `0${day}`; } + const date = `${full_year}/${month}/${day}`; + if (date.length === 8) { + if (!isDate(date, 'YY/MM/DD')) { return false; } + } else if (!isDate(date, 'YYYY/MM/DD')) { return false; } + + return algorithms.luhnCheck(tin.replace(/\W/, '')); +} + +// Locale lookup objects + +/* + * Tax id regex formats for various locales + * + * Where not explicitly specified in DG-TAXUD document both + * uppercase and lowercase letters are acceptable. + */ const taxIdFormat = { + 'bg-BG': /^\d{10}$/, + 'cs-CZ': /^\d{6}\/{0,1}\d{3,4}$/, + 'de-AT': /^\d{9}$/, + 'de-DE': /^[1-9]\d{10}$/, + 'dk-DK': /^\d{6}-{0,1}\d{4}$/, + 'el-CY': /^[09]\d{7}[A-Z]$/, + 'el-GR': /^([0-4]|[7-9])\d{8}$/, + 'en-GB': /^\d{10}$|^(?!GB|NK|TN|ZZ)(?![DFIQUV])[A-Z](?![DFIQUVO])[A-Z]\d{6}[ABCD ]$/i, + 'en-IE': /^\d{7}[A-W][A-IW]{0,1}$/i, 'en-US': /^\d{2}[- ]{0,1}\d{7}$/, + 'es-ES': /^(\d{0,8}|[XYZKLM]\d{7})[A-HJ-NP-TV-Z]$/i, + 'et-EE': /^[1-6]\d{6}(00[1-9]|0[1-9][0-9]|[1-6][0-9]{2}|70[0-9]|710)\d$/, + 'fi-FI': /^\d{6}[-+A]\d{3}[0-9A-FHJ-NPR-Y]$/i, + 'fr-BE': /^\d{11}$/, + 'fr-FR': /^[0-3]\d{12}$|^[0-3]\d\s\d{2}(\s\d{3}){3}$/, // Conforms both to official spec and provided example + 'fr-LU': /^\d{13}$/, + 'hr-HR': /^\d{11}$/, + 'hu-HU': /^8\d{9}$/, + 'it-IT': /^[A-Z]{6}[L-NP-V0-9]{2}[A-EHLMPRST][L-NP-V0-9]{2}[A-ILMZ][L-NP-V0-9]{3}[A-Z]$/i, + 'lv-LV': /^\d{6}-{0,1}\d{5}$/, // Conforms both to DG TAXUD spec and original research + 'mt-MT': /^\d{3,7}[APMGLHBZ]$|^([1-8])\1\d{7}$/i, + 'nl-NL': /^\d{9}$/, + 'pl-PL': /^\d{10,11}$/, + 'pt-PT': /^\d{9}$/, + 'ro-RO': /^\d{13}$/, + 'sk-SK': /^\d{6}\/{0,1}\d{3,4}$/, + 'sl-SI': /^[1-9]\d{7}$/, + 'sv-SE': /^(\d{6}[-+]{0,1}\d{4}|(18|19|20)\d{6}[-+]{0,1}\d{4})$/, }; - +// taxIdFormat locale aliases +taxIdFormat['lb-LU'] = taxIdFormat['fr-LU']; +taxIdFormat['lt-LT'] = taxIdFormat['et-EE']; +taxIdFormat['nl-BE'] = taxIdFormat['fr-BE']; // Algorithmic tax id check functions for various locales const taxIdCheck = { + 'bg-BG': bgBgCheck, + 'cs-CZ': csCzCheck, + 'de-AT': deAtCheck, + 'de-DE': deDeCheck, + 'dk-DK': dkDkCheck, + 'el-CY': elCyCheck, + 'el-GR': elGrCheck, + 'en-IE': enIeCheck, 'en-US': enUsCheck, + 'es-ES': esEsCheck, + 'et-EE': etEeCheck, + 'fi-FI': fiFiCheck, + 'fr-BE': frBeCheck, + 'fr-FR': frFrCheck, + 'fr-LU': frLuCheck, + 'hr-HR': hrHrCheck, + 'hu-HU': huHuCheck, + 'it-IT': itItCheck, + 'lv-LV': lvLvCheck, + 'mt-MT': mtMtCheck, + 'nl-NL': nlNlCheck, + 'pl-PL': plPlCheck, + 'pt-PT': ptPtCheck, + 'ro-RO': roRoCheck, + 'sk-SK': skSkCheck, + 'sl-SI': slSiCheck, + 'sv-SE': svSeCheck, }; +// taxIdCheck locale aliases +taxIdCheck['lb-LU'] = taxIdCheck['fr-LU']; +taxIdCheck['lt-LT'] = taxIdCheck['et-EE']; +taxIdCheck['nl-BE'] = taxIdCheck['fr-BE']; + +// Regexes for locales where characters should be omitted before checking format +const allsymbols = /[-\\\/!@#$%\^&\*\(\)\+\=\[\]]+/g; +const sanitizeRegexes = { + 'de-AT': allsymbols, + 'de-DE': /[\/\\]/g, + 'fr-BE': allsymbols, +}; +// sanitizeRegexes locale aliases +sanitizeRegexes['nl-BE'] = sanitizeRegexes['fr-BE']; /* * Validator function @@ -77,13 +1106,19 @@ const taxIdCheck = { */ export default function isTaxID(str, locale = 'en-US') { assertString(str); + // Copy TIN to avoid replacement if sanitized + let strcopy = str.slice(0); + if (locale in taxIdFormat) { - if (!taxIdFormat[locale].test(str)) { + if (locale in sanitizeRegexes) { + strcopy = strcopy.replace(sanitizeRegexes[locale], ''); + } + if (!taxIdFormat[locale].test(strcopy)) { return false; } if (locale in taxIdCheck) { - return taxIdCheck[locale](str); + return taxIdCheck[locale](strcopy); } // Fallthrough; not all locales have algorithmic checks return true; diff --git a/src/lib/util/algorithms.js b/src/lib/util/algorithms.js new file mode 100644 index 000000000..4ac1f2784 --- /dev/null +++ b/src/lib/util/algorithms.js @@ -0,0 +1,97 @@ +/** + * Algorithmic validation functions + * May be used as is or implemented in the workflow of other validators. + */ + +/* + * ISO 7064 validation function + * Called with a string of numbers (incl. check digit) + * to validate according to ISO 7064 (MOD 11, 10). + */ +export function iso7064Check(str) { + let checkvalue = 10; + for (let i = 0; i < str.length - 1; i++) { + checkvalue = (parseInt(str[i], 10) + checkvalue) % 10 === 0 ? (10 * 2) % 11 : + (((parseInt(str[i], 10) + checkvalue) % 10) * 2) % 11; + } + checkvalue = checkvalue === 1 ? 0 : 11 - checkvalue; + return checkvalue === parseInt(str[10], 10); +} + +/* + * Luhn (mod 10) validation function + * Called with a string of numbers (incl. check digit) + * to validate according to the Luhn algorithm. + */ +export function luhnCheck(str) { + let checksum = 0; + let second = false; + for (let i = str.length - 1; i >= 0; i--) { + if (second) { + const product = parseInt(str[i], 10) * 2; + if (product > 9) { + // sum digits of product and add to checksum + checksum += product.toString().split('').map(a => parseInt(a, 10)).reduce((a, b) => a + b, 0); + } else { + checksum += product; + } + } else { + checksum += parseInt(str[i], 10); + } + second = !second; + } + return checksum % 10 === 0; +} + +/* + * Reverse TIN multiplication and summation helper function + * Called with an array of single-digit integers and a base multiplier + * to calculate the sum of the digits multiplied in reverse. + * Normally used in variations of MOD 11 algorithmic checks. + */ +export function reverseMultiplyAndSum(digits, base) { + let total = 0; + for (let i = 0; i < digits.length; i++) { + total += digits[i] * (base - i); + } + return total; +} + +/* + * Verhoeff validation helper function + * Called with a string of numbers + * to validate according to the Verhoeff algorithm. + */ +export function verhoeffCheck(str) { + const d_table = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 0, 6, 7, 8, 9, 5], + [2, 3, 4, 0, 1, 7, 8, 9, 5, 6], + [3, 4, 0, 1, 2, 8, 9, 5, 6, 7], + [4, 0, 1, 2, 3, 9, 5, 6, 7, 8], + [5, 9, 8, 7, 6, 0, 4, 3, 2, 1], + [6, 5, 9, 8, 7, 1, 0, 4, 3, 2], + [7, 6, 5, 9, 8, 2, 1, 0, 4, 3], + [8, 7, 6, 5, 9, 3, 2, 1, 0, 4], + [9, 8, 7, 6, 5, 4, 3, 2, 1, 0], + ]; + + const p_table = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 5, 7, 6, 2, 8, 3, 0, 9, 4], + [5, 8, 0, 3, 7, 9, 6, 1, 4, 2], + [8, 9, 1, 6, 0, 4, 3, 5, 2, 7], + [9, 4, 5, 3, 1, 2, 6, 8, 7, 0], + [4, 2, 8, 6, 5, 7, 3, 9, 0, 1], + [2, 7, 9, 3, 8, 0, 6, 4, 1, 5], + [7, 0, 4, 6, 9, 1, 3, 2, 5, 8], + ]; + + // Copy (to prevent replacement) and reverse + const str_copy = str.split('').reverse().join(''); + let checksum = 0; + for (let i = 0; i < str_copy.length; i++) { + checksum = d_table[checksum][p_table[i % 8][parseInt(str_copy[i], 10)]]; + } + return checksum === 0; +} diff --git a/test/validators.js b/test/validators.js index cf68f0d36..e6e605d15 100644 --- a/test/validators.js +++ b/test/validators.js @@ -9403,10 +9403,164 @@ describe('Validators', () => { }); }); - + // EU-UK valid numbers sourced from https://ec.europa.eu/taxation_customs/tin/specs/FS-TIN%20Algorithms-Public.docx or constructed by @tplessas. it('should validate taxID', () => { test({ validator: 'isTaxID', + args: ['bg-BG'], + valid: [ + '7501010010', + '0101010012', + '0111010010', + '7521010014', + '7541010019'], + invalid: [ + '750101001', + '75010100101', + '75-01010/01 0', + '7521320010', + '7501010019'], + }); + test({ + validator: 'isTaxID', + args: ['cs-CZ'], + valid: [ + '530121999', + '530121/999', + '530121/9990', + '5301219990', + '1602295134', + '5451219994', + '0424175466', + '0532175468', + '7159079940'], + invalid: [ + '53-0121 999', + '530121000', + '960121999', + '0124175466', + '0472301754', + '1975116400', + '7159079945'], + }); + test({ + validator: 'isTaxID', + args: ['de-AT'], + valid: [ + '931736581', + '93-173/6581', + '93--173/6581'], + invalid: [ + '999999999', + '93 173 6581', + '93-173/65811', + '93-173/658'], + }); + test({ + validator: 'isTaxID', + args: ['de-DE'], + valid: [ + '26954371827', + '86095742719', + '65929970489', + '79608434120', + '659/299/7048/9'], + invalid: [ + '26954371828', + '86095752719', + '8609575271', + '860957527190', + '65299970489', + '65999970489', + '6592997048-9'], + }); + test({ + validator: 'isTaxID', + args: ['dk-DK'], + valid: [ + '010111-1113', + '0101110117', + '2110084008', + '2110489008', + '2110595002', + '2110197007', + '0101110117', + '0101110230'], + invalid: [ + '010111/1113', + '010111111', + '01011111133', + '2110485008', + '2902034000', + '0101110630'], + }); + test({ + validator: 'isTaxID', + args: ['el-CY'], + valid: [ + '00123123T', + '99652156X'], + invalid: [ + '99652156A', + '00124123T', + '00123123', + '001123123T', + '00 12-3123/T'], + }); + test({ + validator: 'isTaxID', + args: ['el-GR'], + valid: [ + '758426713', + '054100004'], + invalid: [ + '054100005', + '05410000', + '0541000055', + '05 4100005', + '05-410/0005', + '658426713', + '558426713'], + }); + test({ + validator: 'isTaxID', + args: ['en-GB'], + valid: [ + '1234567890', + 'AA123456A', + 'AA123456 '], + invalid: [ + 'GB123456A', + '123456789', + '12345678901', + 'NK123456A', + 'TN123456A', + 'ZZ123456A', + 'GB123456Z', + 'DM123456A', + 'AO123456A', + 'GB-123456A', + 'GB 123456 A', + 'GB123456 '], + }); + test({ + validator: 'isTaxID', + args: ['en-IE'], + valid: [ + '1234567T', + '1234567TW', + '1234577W', + '1234577WW', + '1234577IA'], + invalid: [ + '1234567', + '1234577WWW', + '1234577A', + '1234577JA'], + }); + test({ + validator: 'isTaxID', + args: ['en-US'], valid: [ '01-1234567', '01 1234567', @@ -9426,6 +9580,297 @@ describe('Validators', () => { '28-1234567', '96-1234567'], }); + test({ + validator: 'isTaxID', + args: ['es-ES'], + valid: [ + '00054237A', + '54237A', + 'X1234567L', + 'Z1234567R', + 'M2812345C', + 'Y2812345B'], + invalid: [ + 'M2812345CR', + 'A2812345C', + '0/005 423-7A', + '00054237U'], + }); + test({ + validator: 'isTaxID', + args: ['et-EE'], + valid: [ + '10001010080', + '46304280206', + '37102250382', + '32708101201'], + invalid: [ + '46304280205', + '61002293333', + '4-6304 28/0206', + '4630428020', + '463042802066'], + }); + test({ + validator: 'isTaxID', + args: ['fi-FI'], + valid: [ + '131052-308T', + '131002+308W', + '131019A3089'], + invalid: [ + '131052308T', + '131052-308TT', + '131052S308T', + '13 1052-308/T', + '290219A1111'], + }); + test({ + validator: 'isTaxID', + args: ['fr-BE'], + valid: [ + '00012511119'], + }); + test({ + validator: 'isTaxID', + args: ['fr-FR'], + valid: [ + '30 23 217 600 053', + '3023217600053'], + invalid: [ + '30 2 3 217 600 053', + '3 023217-600/053', + '3023217600052', + '3023217500053', + '30232176000534', + '302321760005'], + }); + test({ + validator: 'isTaxID', + args: ['nl-BE'], + valid: [ + '00012511148', + '00/0125-11148', + '00000011115'], + invalid: [ + '00 01 2511148', + '01022911148', + '00013211148', + '0001251114', + '000125111480', + '00012511149'], + }); + test({ + validator: 'isTaxID', + args: ['fr-LU'], + valid: [ + '1893120105732'], + invalid: [ + '189312010573', + '18931201057322', + '1893 12-01057/32', + '1893120105742', + '1893120105733'], + }); + test({ + validator: 'isTaxID', + args: ['lb-LU'], + invalid: [ + '2016023005732'], + }); + test({ + validator: 'isTaxID', + args: ['hr-HR'], + valid: [ + '94577403194'], + invalid: [ + '94 57-7403/194', + '9457740319', + '945774031945', + '94577403197', + '94587403194'], + }); + test({ + validator: 'isTaxID', + args: ['hu-HU'], + valid: [ + '8071592153'], + invalid: [ + '80 71-592/153', + '80715921534', + '807159215', + '8071592152', + '8071582153'], + }); + test({ + validator: 'isTaxID', + args: ['lt-LT'], + valid: [ + '33309240064'], + }); + test({ + validator: 'isTaxID', + args: ['it-IT'], + valid: [ + 'DMLPRY77D15H501F', + 'AXXFAXTTD41H501D'], + invalid: [ + 'DML PRY/77D15H501-F', + 'DMLPRY77D15H501', + 'DMLPRY77D15H501FF', + 'AAPPRY77D15H501F', + 'DMLAXA77D15H501F', + 'AXXFAX90A01Z001F', + 'DMLPRY77B29H501F', + 'AXXFAX3TD41H501E'], + }); + test({ + validator: 'isTaxID', + args: ['lv-LV'], + valid: [ + '01011012344', + '32579461005', + '01019902341', + '325794-61005'], + invalid: [ + '010110123444', + '0101101234', + '01001612345', + '290217-22343'], + }); + test({ + validator: 'isTaxID', + args: ['mt-MT'], + valid: [ + '1234567A', + '882345608', + '34581M', + '199Z'], + invalid: [ + '812345608', + '88234560', + '8823456088', + '11234567A', + '12/34-567 A', + '88 23-456/08', + '1234560A', + '0000000M', + '3200100G'], + }); + test({ + validator: 'isTaxID', + args: ['nl-NL'], + valid: [ + '174559434'], + invalid: [ + '17455943', + '1745594344', + '17 455-94/34'], + }); + test({ + validator: 'isTaxID', + args: ['pl-PL'], + valid: [ + '2234567895', + '02070803628', + '02870803622', + '02670803626', + '01510813623'], + invalid: [ + '020708036285', + '223456789', + '22 345-678/95', + '02 070-8036/28', + '2234567855', + '02223013623'], + }); + test({ + validator: 'isTaxID', + args: ['pt-PT'], + valid: [ + '299999998', + '299992020'], + invalid: [ + '2999999988', + '29999999', + '29 999-999/8'], + }); + test({ + validator: 'isTaxID', + args: ['ro-RO'], + valid: [ + '8001011234563', + '9000123456789', + '1001011234560', + '3001011234564', + '5001011234568'], + invalid: [ + '5001011234569', + '500 1011-234/568', + '500101123456', + '50010112345688', + '5001011504568', + '8000230234563', + '6000230234563'], + }); + test({ + validator: 'isTaxID', + args: ['sk-SK'], + valid: [ + '530121999', + '536221/999', + '031121999', + '520229999', + '1234567890'], + invalid: [ + '53012199999', + '990101999', + '530121000', + '53012199', + '53-0121 999', + '535229999'], + }); + test({ + validator: 'isTaxID', + args: ['sl-SI'], + valid: [ + '15012557', + '15012590'], + invalid: [ + '150125577', + '1501255', + '15 01-255/7'], + }); + test({ + validator: 'isTaxID', + args: ['sv-SE'], + valid: [ + '640823-3234', + '640883-3231', + '6408833231', + '19640823-3233', + '196408233233', + '19640883-3230', + '200228+5266', + '20180101-5581'], + invalid: [ + '640823+3234', + '160230-3231', + '160260-3231', + '160260-323', + '160260323', + '640823+323', + '640823323', + '640823+32344', + '64082332344', + '19640823-32333', + '1964082332333'], + }); + test({ + validator: 'isTaxID', + valid: [ + '01-1234567'], + }); test({ validator: 'isTaxID', args: ['is-NOT'],