Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 56 additions & 41 deletions src/lib/isTaxID.js
Original file line number Diff line number Diff line change
Expand Up @@ -910,9 +910,61 @@ function plPlCheck(tin) {
* pt-BR validation function
* (Cadastro de Pessoas Físicas (CPF, persons)
* Cadastro Nacional de Pessoas Jurídicas (CNPJ, entities)
* Both inputs will be validated
* Both inputs will be validated.
* CNPJ supports both numeric (legacy) and alphanumeric format (starting July 2026).
*/

/**
* Convert a CNPJ character to its numeric value for check digit calculation.
* Numbers 0-9 map to values 0-9, letters A-Z map to values 17-42.
* This is done by subtracting 48 from the ASCII code.
*/
function cnpjCharToValue(char) {
return char.charCodeAt(0) - 48;
}

/**
* Validate CNPJ (both numeric and alphanumeric formats).
* Algorithm: module 11 with weights 2-9 from right to left.
*/
function validateCnpj(cnpj) {
// Get the 12 identifier characters and 2 check digits
const identifiers = cnpj.substring(0, 12).toUpperCase();
const checkDigits = cnpj.substring(12);

// Reject CNPJs with all same characters (e.g., '00000000000000', 'AAAAAAAAAAAAAA')
if (/^(.)\1+$/.test(cnpj.toUpperCase())) {
return false;
}

// Calculate first check digit
let sum = 0;
let weight = 5;
for (let i = 0; i < 12; i++) {
sum += cnpjCharToValue(identifiers.charAt(i)) * weight;
weight = weight === 2 ? 9 : weight - 1;
}
let remainder = sum % 11;
let firstDV = remainder < 2 ? 0 : 11 - remainder;

if (firstDV !== parseInt(checkDigits.charAt(0), 10)) {
return false;
}

// Calculate second check digit (includes first check digit)
sum = 0;
weight = 6;
for (let i = 0; i < 12; i++) {
sum += cnpjCharToValue(identifiers.charAt(i)) * weight;
weight = weight === 2 ? 9 : weight - 1;
}
sum += firstDV * 2;
remainder = sum % 11;
let secondDV = remainder < 2 ? 0 : 11 - remainder;

return secondDV === parseInt(checkDigits.charAt(1), 10);
}

function ptBrCheck(tin) {
if (tin.length === 11) {
let sum;
Expand Down Expand Up @@ -946,45 +998,8 @@ function ptBrCheck(tin) {
return true;
}

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;
// CNPJ validation (supports both numeric and alphanumeric formats)
return validateCnpj(tin);
}

/*
Expand Down Expand Up @@ -1190,7 +1205,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}$)|(?:^\d{14}$)/,
'pt-BR': /(?:^\d{11}$)|(?:^[A-Z0-9]{12}\d{2}$)/i,
'pt-PT': /^\d{9}$/,
'ro-RO': /^\d{13}$/,
'sk-SK': /^\d{6}\/{0,1}\d{3,4}$/,
Expand Down
14 changes: 13 additions & 1 deletion test/validators.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13994,10 +13994,16 @@ describe('Validators', () => {
validator: 'isTaxID',
args: ['pt-BR'],
valid: [
// CPF (persons)
'35161990910',
'74407265027',
// CNPJ numeric (legacy format)
'05423994000172',
'11867044000130'],
'11867044000130',
// CNPJ alphanumeric (new format starting July 2026)
'12ABC34501DE35', // Example from official SERPRO documentation
'12abc34501de35', // Lowercase should also work
],
invalid: [
'ABCDEFGH',
'170.691.440-72',
Expand All @@ -14011,6 +14017,12 @@ describe('Validators', () => {
'111111111111112',
'61938188550993',
'82168365502729',
// Invalid alphanumeric CNPJs
'12ABC34501DE00', // Wrong check digits
'12ABC34501DE99', // Wrong check digits
'AAAAAAAAAAAAAA', // All same characters
'00000000000000', // All zeros
'12.ABC.345/01DE-35', // Formatted (not accepted)
],
});
test({
Expand Down
Loading