diff --git a/CHANGELOG.md b/CHANGELOG.md index ae3d7ea..41d71ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +- Fix to validate alphanumeric CNPJ in my account + ## [3.19.3] - 2025-10-30 ### Fixed diff --git a/react/__tests__/BRA.test.js b/react/__tests__/BRA.test.js new file mode 100644 index 0000000..5b8955f --- /dev/null +++ b/react/__tests__/BRA.test.js @@ -0,0 +1,52 @@ +import BRA from '../rules/BRA' + +const getCnpjField = () => + BRA.businessFields.find(field => field.name === 'corporateDocument') + +describe('BRA corporateDocument (CNPJ)', () => { + const { mask, validate } = getCnpjField() + + describe('mask', () => { + it('masks numeric CNPJ', () => { + expect(mask('11222333000181')).toBe('11.222.333/0001-81') + }) + + it('masks alphanumeric CNPJ', () => { + expect(mask('12ABC34501DE35')).toBe('12.ABC.345/01DE-35') + }) + + it('masks lowercase alphanumeric input', () => { + expect(mask('12abc34501de35')).toBe('12.abc.345/01de-35') + }) + }) + + describe('validate', () => { + it('accepts valid numeric CNPJ', () => { + expect(validate('11.222.333/0001-81')).toBe(true) + }) + + it('accepts valid alphanumeric CNPJ', () => { + expect(validate('12.ABC.345/01DE-35')).toBe(true) + }) + + it('accepts unformatted valid CNPJ', () => { + expect(validate('11222333000181')).toBe(true) + }) + + it('rejects CNPJ with wrong length', () => { + expect(validate('11.222.333/0001')).toBe(false) + }) + + it('rejects numeric CNPJ with invalid check digits', () => { + expect(validate('11.222.333/0001-00')).toBe(false) + }) + + it('rejects alphanumeric CNPJ with invalid check digits', () => { + expect(validate('12.ABC.345/01DE-00')).toBe(false) + }) + + it('rejects repeated characters', () => { + expect(validate('00.000.000/0000-00')).toBe(false) + }) + }) +}) diff --git a/react/rules/BRA.js b/react/rules/BRA.js index ce8403e..dc1b80e 100644 --- a/react/rules/BRA.js +++ b/react/rules/BRA.js @@ -91,37 +91,28 @@ export default { name: 'corporateDocument', maxLength: 30, label: 'BRA_cnpj', - mask: value => msk.fit(value, '99.999.999/9999-99'), + mask: value => msk.fit(value, 'SS.SSS.SSS/SSSS-99'), validate: value => { - const cleanValue = value.replace(/[^\d]/g, '') - if (cleanValue.length != 14) return false - - const isRepeatedNum = '0123456789' - .split('') - .some(digit => digit.repeat(14) === cleanValue) - if (isRepeatedNum) return false - - const firstWeights = '543298765432'.split('') - const firstReduce = cleanValue - .split('') + const cleanValue = value.replace(/[.\/-]/g, '').toUpperCase() + if (cleanValue.length !== 14) return false + if (!/^[A-Z0-9]{12}\d{2}$/.test(cleanValue)) return false + + if (/^(.)\1+$/.test(cleanValue)) return false + + const getCharValue = char => char.charCodeAt(0) - 48 + const values = cleanValue.split('').map(getCharValue) + + const firstWeights = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] + const firstReduce = values .slice(0, 12) - .reduce( - (acc, cur, index) => - acc + parseInt(cur) * parseInt(firstWeights[index]), - 0, - ) + .reduce((acc, cur, index) => acc + cur * firstWeights[index], 0) const firstDigit = firstReduce % 11 < 2 ? 0 : 11 - (firstReduce % 11) if (firstDigit != cleanValue.charAt(12)) return false - - const secondWeights = ['6', ...firstWeights] - const secondReduce = cleanValue - .split('') + + const secondWeights = [6, ...firstWeights] + const secondReduce = values .slice(0, 13) - .reduce( - (acc, cur, index) => - acc + parseInt(cur) * parseInt(secondWeights[index]), - 0, - ) + .reduce((acc, cur, index) => acc + cur * secondWeights[index], 0) const secondDigit = secondReduce % 11 < 2 ? 0 : 11 - (secondReduce % 11) return secondDigit == cleanValue.charAt(13) },