Skip to content

Commit 90b0a9a

Browse files
authored
fix(isTaxID): improve pt-BR locale by adding support for alphanumeric CNPJ format (#2644)
1 parent f39bb3b commit 90b0a9a

File tree

2 files changed

+69
-42
lines changed

2 files changed

+69
-42
lines changed

src/lib/isTaxID.js

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -910,9 +910,61 @@ function plPlCheck(tin) {
910910
* pt-BR validation function
911911
* (Cadastro de Pessoas Físicas (CPF, persons)
912912
* Cadastro Nacional de Pessoas Jurídicas (CNPJ, entities)
913-
* Both inputs will be validated
913+
* Both inputs will be validated.
914+
* CNPJ supports both numeric (legacy) and alphanumeric format (starting July 2026).
914915
*/
915916

917+
/**
918+
* Convert a CNPJ character to its numeric value for check digit calculation.
919+
* Numbers 0-9 map to values 0-9, letters A-Z map to values 17-42.
920+
* This is done by subtracting 48 from the ASCII code.
921+
*/
922+
function cnpjCharToValue(char) {
923+
return char.charCodeAt(0) - 48;
924+
}
925+
926+
/**
927+
* Validate CNPJ (both numeric and alphanumeric formats).
928+
* Algorithm: module 11 with weights 2-9 from right to left.
929+
*/
930+
function validateCnpj(cnpj) {
931+
// Get the 12 identifier characters and 2 check digits
932+
const identifiers = cnpj.substring(0, 12).toUpperCase();
933+
const checkDigits = cnpj.substring(12);
934+
935+
// Reject CNPJs with all same characters (e.g., '00000000000000', 'AAAAAAAAAAAAAA')
936+
if (/^(.)\1+$/.test(cnpj.toUpperCase())) {
937+
return false;
938+
}
939+
940+
// Calculate first check digit
941+
let sum = 0;
942+
let weight = 5;
943+
for (let i = 0; i < 12; i++) {
944+
sum += cnpjCharToValue(identifiers.charAt(i)) * weight;
945+
weight = weight === 2 ? 9 : weight - 1;
946+
}
947+
let remainder = sum % 11;
948+
let firstDV = remainder < 2 ? 0 : 11 - remainder;
949+
950+
if (firstDV !== parseInt(checkDigits.charAt(0), 10)) {
951+
return false;
952+
}
953+
954+
// Calculate second check digit (includes first check digit)
955+
sum = 0;
956+
weight = 6;
957+
for (let i = 0; i < 12; i++) {
958+
sum += cnpjCharToValue(identifiers.charAt(i)) * weight;
959+
weight = weight === 2 ? 9 : weight - 1;
960+
}
961+
sum += firstDV * 2;
962+
remainder = sum % 11;
963+
let secondDV = remainder < 2 ? 0 : 11 - remainder;
964+
965+
return secondDV === parseInt(checkDigits.charAt(1), 10);
966+
}
967+
916968
function ptBrCheck(tin) {
917969
if (tin.length === 11) {
918970
let sum;
@@ -946,45 +998,8 @@ function ptBrCheck(tin) {
946998
return true;
947999
}
9481000

949-
if ( // Reject know invalid CNPJs
950-
tin === '00000000000000' ||
951-
tin === '11111111111111' ||
952-
tin === '22222222222222' ||
953-
tin === '33333333333333' ||
954-
tin === '44444444444444' ||
955-
tin === '55555555555555' ||
956-
tin === '66666666666666' ||
957-
tin === '77777777777777' ||
958-
tin === '88888888888888' ||
959-
tin === '99999999999999') { return false; }
960-
961-
let length = tin.length - 2;
962-
let identifiers = tin.substring(0, length);
963-
let verificators = tin.substring(length);
964-
let sum = 0;
965-
let pos = length - 7;
966-
967-
for (let i = length; i >= 1; i--) {
968-
sum += identifiers.charAt(length - i) * pos;
969-
pos -= 1;
970-
if (pos < 2) { pos = 9; }
971-
}
972-
let result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
973-
if (result !== parseInt(verificators.charAt(0), 10)) { return false; }
974-
975-
length += 1;
976-
identifiers = tin.substring(0, length);
977-
sum = 0;
978-
pos = length - 7;
979-
for (let i = length; i >= 1; i--) {
980-
sum += identifiers.charAt(length - i) * pos;
981-
pos -= 1;
982-
if (pos < 2) { pos = 9; }
983-
}
984-
result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
985-
if (result !== parseInt(verificators.charAt(1), 10)) { return false; }
986-
987-
return true;
1001+
// CNPJ validation (supports both numeric and alphanumeric formats)
1002+
return validateCnpj(tin);
9881003
}
9891004

9901005
/*
@@ -1190,7 +1205,7 @@ const taxIdFormat = {
11901205
'mt-MT': /^\d{3,7}[APMGLHBZ]$|^([1-8])\1\d{7}$/i,
11911206
'nl-NL': /^\d{9}$/,
11921207
'pl-PL': /^\d{10,11}$/,
1193-
'pt-BR': /(?:^\d{11}$)|(?:^\d{14}$)/,
1208+
'pt-BR': /(?:^\d{11}$)|(?:^[A-Z0-9]{12}\d{2}$)/i,
11941209
'pt-PT': /^\d{9}$/,
11951210
'ro-RO': /^\d{13}$/,
11961211
'sk-SK': /^\d{6}\/{0,1}\d{3,4}$/,

test/validators.test.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13954,10 +13954,16 @@ describe('Validators', () => {
1395413954
validator: 'isTaxID',
1395513955
args: ['pt-BR'],
1395613956
valid: [
13957+
// CPF (persons)
1395713958
'35161990910',
1395813959
'74407265027',
13960+
// CNPJ numeric (legacy format)
1395913961
'05423994000172',
13960-
'11867044000130'],
13962+
'11867044000130',
13963+
// CNPJ alphanumeric (new format starting July 2026)
13964+
'12ABC34501DE35', // Example from official SERPRO documentation
13965+
'12abc34501de35', // Lowercase should also work
13966+
],
1396113967
invalid: [
1396213968
'ABCDEFGH',
1396313969
'170.691.440-72',
@@ -13971,6 +13977,12 @@ describe('Validators', () => {
1397113977
'111111111111112',
1397213978
'61938188550993',
1397313979
'82168365502729',
13980+
// Invalid alphanumeric CNPJs
13981+
'12ABC34501DE00', // Wrong check digits
13982+
'12ABC34501DE99', // Wrong check digits
13983+
'AAAAAAAAAAAAAA', // All same characters
13984+
'00000000000000', // All zeros
13985+
'12.ABC.345/01DE-35', // Formatted (not accepted)
1397413986
],
1397513987
});
1397613988
test({

0 commit comments

Comments
 (0)