Repository: validatorjs/validator.js
Pull request #1613 (2021-02-21 12:53:04)

feat(pt-BR): tax id, passport and license plates
Included Tax ID, passport, and license plate validators for `pt-BR` (Brazil) locale. Tests written and passed. Locale added to the documentation.

## Checklist

- [ X ] PR contains only changes related; no stray files, etc.
- [ X ] README updated (where applicable)
- [ X ] Tests written (where applicable)


renanmontebelo: There's some overlap with these already opened PRs:

- https://github.com/validatorjs/validator.js/pull/1589
- https://github.com/validatorjs/validator.js/pull/1588

We might consider to join forces if you wish.
mschunke: Sure, for what I see @renanmontebelo license plate approach is more complete as "mercosur" plates are available too. My approach for tax Id validation uses the current `.isTaxId` function and supports also id (CNPJ) validation for entities and well as CPF (persons) validations.

I would suggest to keep his for license plates and mine for tax Ids.
tux-tn: Thank you for your contribution @mschunke. Can you please resolve merge conflicts (delete `validator.js` and `validator.min.js` files and fix conflict in `README.md` ) and that would be great if you join forces and decide what to keep in each PR
codecov[bot]: # [Codecov](https://codecov.io/gh/validatorjs/validator.js/pull/1613?src=pr&el=h1) Report
> Merging [#1613](https://codecov.io/gh/validatorjs/validator.js/pull/1613?src=pr&el=desc) (a73a9f0) into [master](https://codecov.io/gh/validatorjs/validator.js/commit/deb1d1ef79fd60dfc60e7894db345df51eb409ee?el=desc) (deb1d1e) will **decrease** coverage by `0.10%`.
> The diff coverage is `95.34%`.

[![Impacted file tree graph](https://codecov.io/gh/validatorjs/validator.js/pull/1613/graphs/tree.svg?width=650&height=150&src=pr&token=YzR3kbN1m2)](https://codecov.io/gh/validatorjs/validator.js/pull/1613?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##            master    #1613      +/-   ##
===========================================
- Coverage   100.00%   99.89%   -0.11%     
===========================================
  Files          100      100              
  Lines         1807     1850      +43     
===========================================
+ Hits          1807     1848      +41     
- Misses           0        2       +2     
```


| [Impacted Files](https://codecov.io/gh/validatorjs/validator.js/pull/1613?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/lib/isPassportNumber.js](https://codecov.io/gh/validatorjs/validator.js/pull/1613/diff?src=pr&el=tree#diff-c3JjL2xpYi9pc1Bhc3Nwb3J0TnVtYmVyLmpz) | `100.00% <ø> (ø)` | |
| [src/lib/isTaxID.js](https://codecov.io/gh/validatorjs/validator.js/pull/1613/diff?src=pr&el=tree#diff-c3JjL2xpYi9pc1RheElELmpz) | `99.56% <95.23%> (-0.44%)` | :arrow_down: |
| [src/lib/isLicensePlate.js](https://codecov.io/gh/validatorjs/validator.js/pull/1613/diff?src=pr&el=tree#diff-c3JjL2xpYi9pc0xpY2Vuc2VQbGF0ZS5qcw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/validatorjs/validator.js/pull/1613?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/validatorjs/validator.js/pull/1613?src=pr&el=footer). Last update [deb1d1e...5da17cd](https://codecov.io/gh/validatorjs/validator.js/pull/1613?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

murilopulley: Hey @tux-tn, I suppose all conflicts are solved.
mschunke: Hey @tux-tn, all conflicts are solved and build checks passed. Anything else I should do?
tux-tn: @renanmontebelo @mschunke what have you decided concerning what approach to keep?
Should this one add only tax id and passport and the other one add license plate validation?
mschunke: > @renanmontebelo @mschunke what have you decided concerning what approach to keep?
> Should this one add only tax id and passport and the other one add license plate validation?
Hey, @tux-tn,
Got no reply from @renanmontebelo, but I'm fine with this PR just adding tax id and passport and using the other PR for license plates.
mschunke: > Thank you @mschunke ! I added two comments concerning untested conditions/code branches. Can you check it?
@tux-tn 
Was wondering where those untested lines were... thank you! Code updated with 100% coverage.
renanmontebelo: @mschunke sorry for the delay, I can only work on this in the weekends. Selecting your PR for tax id and my PR for license plates sounds good. I'll close https://github.com/validatorjs/validator.js/pull/1589 in favor of yours.

My tax id implementation also accepts an optional parameter to require or ignore formatting characters like `.` and `-`. Please feel free to copy into yours if you think it's a good addition. Thank you.
mschunke: @renanmontebelo np. We all work on these on our spare time. I've removed my license plate validation for we can use yours, and kept my tax ID validator as agreed. Thanks for the cooperation!

btw, my tax id validator accepts both formatted and unformatted input.
renanmontebelo: Sounds good, thank you for the cooperation as well. I missed that yours accept both formatted/unformatted CPFs, that's awesome.
mschunke: @tux-tn sorry about that, forget about the readme file! File updated. Please let me know if you need anything else 😁

Patch:
diff --git a/README.md b/README.md
index ab5609a..cde5072 100644
--- a/README.md
+++ b/README.md
@@ -143,7 +143,7 @@ Validator                               | Description
 **isMultibyte(str)**                    | check if the string contains one or more multibyte chars.
 **isNumeric(str [, options])**                      | check if the string contains only numbers.<br/><br/>`options` is an object which defaults to `{no_symbols: false}` it also has locale as an option. If `no_symbols` is true, the validator will reject numeric strings that feature a symbol (e.g. `+`, `-`, or `.`).<br/><br/>`locale` determine the decimal separator and is one of `['ar', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', 'ar-JO', 'ar-KW', 'ar-LB', 'ar-LY', 'ar-MA', 'ar-QA', 'ar-QM', 'ar-SA', 'ar-SD', 'ar-SY', 'ar-TN', 'ar-YE', 'bg-BG', 'cs-CZ', 'da-DK', 'de-DE', 'en-AU', 'en-GB', 'en-HK', 'en-IN', 'en-NZ', 'en-US', 'en-ZA', 'en-ZM', 'es-ES', 'fr-FR', 'fr-CA', 'hu-HU', 'it-IT', 'nb-NO', 'nl-NL', 'nn-NO', 'pl-PL', 'pt-BR', 'pt-PT', 'ru-RU', 'sl-SI', 'sr-RS', 'sr-RS@latin', 'sv-SE', 'tr-TR', 'uk-UA']`.
 **isOctal(str)**                        | check if the string is a valid octal number.
-**isPassportNumber(str, countryCode)**    | check if the string is a valid passport number.<br/><br/>(countryCode is one of `[ 'AM', 'AR', 'AT', 'AU', 'BE', 'BG', 'BY', 'CA', 'CH', 'CN', 'CY', 'CZ', 'DE', 'DK', 'DZ', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE' 'IN', 'IS', 'IT', 'JP', 'KR', 'LT', 'LU', 'LV', 'LY', 'MT', 'NL', 'PO', 'PT', 'RO', 'RU', 'SE', 'SL', 'SK', 'TR', 'UA', 'US' ]`.
+**isPassportNumber(str, countryCode)**    | check if the string is a valid passport number.<br/><br/>(countryCode is one of `[ 'AM', 'AR', 'AT', 'AU', 'BE', 'BG', 'BY', 'BR', 'CA', 'CH', 'CN', 'CY', 'CZ', 'DE', 'DK', 'DZ', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE' 'IN', 'IS', 'IT', 'JP', 'KR', 'LT', 'LU', 'LV', 'LY', 'MT', 'NL', 'PO', 'PT', 'RO', 'RU', 'SE', 'SL', 'SK', 'TR', 'UA', 'US' ]`.
 **isPort(str)**                         | check if the string is a valid port number.
 **isPostalCode(str, locale)**           | check if the string is a postal code,<br/><br/>(locale is one of `[ 'AD', 'AT', 'AU', 'AZ', 'BE', 'BG', 'BR', 'BY', 'CA', 'CH', 'CN', 'CZ', 'DE', 'DK', 'DO', 'DZ', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HT', 'HU', 'ID', 'IE' 'IL', 'IN', 'IR', 'IS', 'IT', 'JP', 'KE', 'LI', 'LT', 'LU', 'LV', 'MT', 'MX', 'MY', 'NL', 'NO', 'NP', 'NZ', 'PL', 'PR', 'PT', 'RO', 'RU', 'SA', 'SE', 'SG', 'SI', 'TH', 'TN', 'TW', 'UA', 'US', 'ZA', 'ZM' ]` OR 'any'. If 'any' is used, function will check if any of the locals match. Locale list is `validator.isPostalCodeLocales`.).
 **isRFC3339(str)**                      | check if the string is a valid [RFC 3339](https://tools.ietf.org/html/rfc3339) date.
@@ -153,7 +153,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.<br/>Default options: <br/>`{ 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`.<br/><br/>More info about exact TIN support can be found in `src/lib/isTaxID.js`<br/><br/>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' ]`
+**isTaxID(str, locale)**                | Check if the given value is a valid Tax Identification    Number. Default locale is `en-US`.<br/><br/>More info about exact TIN support can be found in `src/lib/isTaxID.js`<br/><br/>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-BR', 'pt-PT', 'ro-RO', 'sk-SK', 'sl-SI', 'sv-SE' ]`
 **isURL(str [, options])**              | check if the string is an URL.<br/><br/>`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 }`.<br/><br/>require_protocol - if set as true isURL will return false if protocol is not present in the URL.<br/>require_valid_protocol - isURL will check if the URL's protocol is present in the protocols option.<br/>protocols - valid protocols can be modified with this option.<br/>require_host - if set as false isURL will not check if host is present in the URL.<br/>require_port - if set as true isURL will check if port is present in the URL.<br/>allow_protocol_relative_urls - if set as true protocol relative URLs will be allowed.<br/>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/isLicensePlate.js b/src/lib/isLicensePlate.js
index c4b010d..db182bf 100644
--- a/src/lib/isLicensePlate.js
+++ b/src/lib/isLicensePlate.js
@@ -16,11 +16,10 @@ export default function isLicensePlate(str, locale) {
     return validators[locale](str);
   } else if (locale === 'any') {
     for (const key in validators) {
-      if (validators.hasOwnProperty(key)) {
-        const validator = validators[key];
-        if (validator(str)) {
-          return true;
-        }
+      /* eslint guard-for-in: 0 */
+      const validator = validators[key];
+      if (validator(str)) {
+        return true;
       }
     }
     return false;
diff --git a/src/lib/isMobilePhone.js b/src/lib/isMobilePhone.js
index 68a1992..207dae7 100644
--- a/src/lib/isMobilePhone.js
+++ b/src/lib/isMobilePhone.js
@@ -56,7 +56,7 @@ const phones = {
   'en-ZW': /^(\+263)[0-9]{9}$/,
   'es-AR': /^\+?549(11|[2368]\d)\d{8}$/,
   'es-BO': /^(\+?591)?(6|7)\d{7}$/,
-  'es-CO': /^(\+?57)?([1-8]{1}|3[0-9]{2})?[0-9]{1}\d{6}$/,
+  'es-CO': /^(\+?57)?3(0(0|1|2|4|5)|1\d|2[0-4]|5(0|1))\d{7}$/,
   'es-CL': /^(\+?56|0)[2-9]\d{1}\d{7}$/,
   'es-CR': /^(\+506)?[2-8]\d{7}$/,
   'es-DO': /^(\+?1)?8[024]9\d{7}$/,
diff --git a/src/lib/isPassportNumber.js b/src/lib/isPassportNumber.js
index 76fd082..0c1582e 100644
--- a/src/lib/isPassportNumber.js
+++ b/src/lib/isPassportNumber.js
@@ -13,6 +13,7 @@ const passportRegexByCountryCode = {
   AU: /^[A-Z]\d{7}$/, // AUSTRALIA
   BE: /^[A-Z]{2}\d{6}$/, // BELGIUM
   BG: /^\d{9}$/, // BULGARIA
+  BR: /^[A-Z]{2}\d{6}$/, // BRAZIL
   BY: /^[A-Z]{2}\d{7}$/, // BELARUS
   CA: /^[A-Z]{2}\d{6}$/, // CANADA
   CH: /^[A-Z]\d{7}$/, // SWITZERLAND
diff --git a/src/lib/isStrongPassword.js b/src/lib/isStrongPassword.js
index e18a759..28bb063 100644
--- a/src/lib/isStrongPassword.js
+++ b/src/lib/isStrongPassword.js
@@ -49,6 +49,7 @@ function analyzePassword(password) {
     symbolCount: 0,
   };
   Object.keys(charMap).forEach((char) => {
+    /* istanbul ignore else */
     if (upperCaseRegex.test(char)) {
       analysis.uppercaseCount += charMap[char];
     } else if (lowerCaseRegex.test(char)) {
diff --git a/src/lib/isTaxID.js b/src/lib/isTaxID.js
index 90a0c2a..f1ebae6 100644
--- a/src/lib/isTaxID.js
+++ b/src/lib/isTaxID.js
@@ -852,6 +852,93 @@ function plPlCheck(tin) {
   return checksum === parseInt(tin[10], 10);
 }
 
+/*
+* pt-BR validation function
+* (Cadastro de Pessoas Físicas (CPF, persons)
+* Cadastro Nacional de Pessoas Jurídicas (CNPJ, entities)
+* Both inputs will be validated
+*/
+
+function ptBrCheck(tin) {
+  tin = tin.replace(/[^\d]+/g, '');
+  if (tin === '') return false;
+
+  if (tin.length === 11) {
+    let sum;
+    let ramainder;
+    sum = 0;
+    tin = tin.replace(/[^\d]+/g, '');
+
+    if ( // Reject known invalid CPFs
+      tin === '11111111111' ||
+      tin === '22222222222' ||
+      tin === '33333333333' ||
+      tin === '44444444444' ||
+      tin === '55555555555' ||
+      tin === '66666666666' ||
+      tin === '77777777777' ||
+      tin === '88888888888' ||
+      tin === '99999999999' ||
+      tin === '00000000000'
+    ) return false;
+
+    for (let i = 1; i <= 9; i++) sum += parseInt(tin.substring(i - 1, i), 10) * (11 - i);
+    ramainder = (sum * 10) % 11;
+    if ((ramainder === 10) || (ramainder === 11)) ramainder = 0;
+    if (ramainder !== parseInt(tin.substring(9, 10), 10)) return false;
+    sum = 0;
+
+    for (let i = 1; i <= 10; i++) sum += parseInt(tin.substring(i - 1, i), 10) * (12 - i);
+    ramainder = (sum * 10) % 11;
+    if ((ramainder === 10) || (ramainder === 11)) ramainder = 0;
+    if (ramainder !== parseInt(tin.substring(10, 11), 10)) return false;
+
+    return true;
+  }
+
+  if (tin.length !== 14) { return false; }
+
+  if ( // Reject know invalid CNPJs
+    tin === '00000000000000' ||
+    tin === '11111111111111' ||
+    tin === '22222222222222' ||
+    tin === '33333333333333' ||
+    tin === '44444444444444' ||
+    tin === '55555555555555' ||
+    tin === '66666666666666' ||
+    tin === '77777777777777' ||
+    tin === '88888888888888' ||
+    tin === '99999999999999') { return false; }
+
+  let length = tin.length - 2;
+  let identifiers = tin.substring(0, length);
+  let verificators = tin.substring(length);
+  let sum = 0;
+  let pos = length - 7;
+
+  for (let i = length; i >= 1; i--) {
+    sum += identifiers.charAt(length - i) * pos;
+    pos -= 1;
+    if (pos < 2) { pos = 9; }
+  }
+  let result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
+  if (result !== parseInt(verificators.charAt(0), 10)) { return false; }
+
+  length += 1;
+  identifiers = tin.substring(0, length);
+  sum = 0;
+  pos = length - 7;
+  for (let i = length; i >= 1; i--) {
+    sum += identifiers.charAt(length - i) * pos;
+    pos -= 1;
+    if (pos < 2) { pos = 9; }
+  }
+  result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
+  if (result !== parseInt(verificators.charAt(1), 10)) { return false; }
+
+  return true;
+}
+
 /*
  * pt-PT validation function
  * (Número de identificação fiscal (NIF), persons/entities)
@@ -1039,6 +1126,7 @@ const taxIdFormat = {
   'mt-MT': /^\d{3,7}[APMGLHBZ]$|^([1-8])\1\d{7}$/i,
   'nl-NL': /^\d{9}$/,
   'pl-PL': /^\d{10,11}$/,
+  'pt-BR': /^\d{11,14}$/,
   'pt-PT': /^\d{9}$/,
   'ro-RO': /^\d{13}$/,
   'sk-SK': /^\d{6}\/{0,1}\d{3,4}$/,
@@ -1076,6 +1164,7 @@ const taxIdCheck = {
   'mt-MT': mtMtCheck,
   'nl-NL': nlNlCheck,
   'pl-PL': plPlCheck,
+  'pt-BR': ptBrCheck,
   'pt-PT': ptPtCheck,
   'ro-RO': roRoCheck,
   'sk-SK': skSkCheck,


Test patch:
diff --git a/test/validators.js b/test/validators.js
index 34b6474..310cc8e 100644
--- a/test/validators.js
+++ b/test/validators.js
@@ -2289,6 +2289,18 @@ describe('Validators', () => {
       ],
     });
 
+    test({
+      validator: 'isPassportNumber',
+      args: ['BR'],
+      valid: [
+        'FZ973689',
+        'GH231233',
+      ],
+      invalid: [
+        'ABX29332',
+      ],
+    });
+
     test({
       validator: 'isPassportNumber',
       args: ['BY'],
@@ -6506,18 +6518,11 @@ describe('Validators', () => {
         valid: [
           '+573003321235',
           '573003321235',
-          '579871235',
           '3003321235',
           '3213321235',
           '3103321235',
-          '3253321235',
-          '3321235',
-          '574321235',
-          '5784321235',
-          '5784321235',
-          '9821235',
+          '3243321235',
           '573011140876',
-          '0698345',
         ],
         invalid: [
           '1234',
@@ -6530,6 +6535,13 @@ describe('Validators', () => {
           '5703013347567',
           '069834567',
           '969834567',
+          '579871235',
+          '574321235',
+          '5784321235',
+          '5784321235',
+          '9821235',
+          '0698345',
+          '3321235',
         ],
       },
       {
@@ -9965,6 +9977,22 @@ describe('Validators', () => {
         '2234567855',
         '02223013623'],
     });
+    test({
+      validator: 'isTaxID',
+      args: ['pt-BR'],
+      valid: [
+        '35161990910',
+        '74407265027',
+        '05423994000172',
+        '11867044000130'],
+      invalid: [
+        '170.691.440-72',
+        '01494282042',
+        '11111111111',
+        '94.592.973/0001-82',
+        '28592361000192',
+        '11111111111111'],
+    });
     test({
       validator: 'isTaxID',
       args: ['pt-PT'],