diff --git a/src/main/java/br/ufsc/bridge/platform/validation/rules/CnpjRule.java b/src/main/java/br/ufsc/bridge/platform/validation/rules/CnpjRule.java index 15d240f..7e0a17a 100644 --- a/src/main/java/br/ufsc/bridge/platform/validation/rules/CnpjRule.java +++ b/src/main/java/br/ufsc/bridge/platform/validation/rules/CnpjRule.java @@ -5,7 +5,19 @@ public class CnpjRule implements Rule { + // Constantes estruturais do CNPJ + private static final int TAMANHO_BASE_CNPJ = 12; + private static final int DIVISOR_MODULO_11 = 11; + + // A regra oficial diz para extrair 48 do valor ASCII do caractere alfanumérico + private static final int OFFSET_ASCII = 48; + + // Pesos para o cálculo dos Dígitos Verificadores + private static final int[] PESOS_DV1 = {5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2}; + private static final int[] PESOS_DV2 = {6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2}; + @Override public String validate(String value) { + // Ignora valores vazios pois existe a regra do "Required" separada if (!Util.isEmpty(value) && !this.isValid(value)) { return "Campo inválido"; } @@ -14,52 +26,53 @@ public class CnpjRule implements Rule { } public boolean isValid(String cnpjValue) { - if (!cnpjValue.matches("^[0-9]*$")) { + if (cnpjValue == null || cnpjValue.length() != 14) { return false; } - if (cnpjValue == "00000000000000" - || cnpjValue == "11111111111111" - || cnpjValue == "22222222222222" - || cnpjValue == "33333333333333" - || cnpjValue == "44444444444444" - || cnpjValue == "55555555555555" - || cnpjValue == "66666666666666" - || cnpjValue == "77777777777777" - || cnpjValue == "88888888888888" - || cnpjValue == "99999999999999" - || cnpjValue.length() != 14) { + + // 1. Limpeza e Normalização: Remove pontuações e garante maiúsculas + String cnpjLimpo = cnpjValue.replaceAll("[^a-zA-Z\\d]", "").toUpperCase(); + + // 2. Validação Estrutural (Regex) + // Exige 12 caracteres alfanuméricos seguidos obrigatoriamente por 2 numéricos + if (!cnpjLimpo.matches("^[A-Z\\d]{12}\\d{2}$")) { return false; } - int tamanho = cnpjValue.length() - 2; - String numeros = cnpjValue.substring(0, tamanho); - String digitos = cnpjValue.substring(tamanho); + + // 3. Extrai os dígitos verificadores informados na string + int dv1Informado = Character.getNumericValue(cnpjLimpo.charAt(12)); + int dv2Informado = Character.getNumericValue(cnpjLimpo.charAt(13)); + + // 4. Calcula os dígitos reais esperados com base na raiz + int dv1Calculado = calcularDigitoVerificador(cnpjLimpo, PESOS_DV1, TAMANHO_BASE_CNPJ); + int dv2Calculado = calcularDigitoVerificador(cnpjLimpo, PESOS_DV2, TAMANHO_BASE_CNPJ + 1); + + // 5. O CNPJ só é válido se ambos os dígitos baterem + return (dv1Informado == dv1Calculado) && (dv2Informado == dv2Calculado); + } + + /** + * Calcula o dígito verificador aplicando o Módulo 11 com a regra alfanumérica. + */ + private static int calcularDigitoVerificador(String cnpj, int[] pesos, int tamanhoConsiderado) { int soma = 0; - int pos = tamanho - 7; - for (int i = tamanho; i >= 1; i--) { - soma += Integer.parseInt(numeros.charAt(tamanho - i) + "") * pos--; - if (pos < 2) { - pos = 9; - } - } - int resultado = soma % 11 < 2 ? 0 : 11 - soma % 11; - if (resultado != Integer.parseInt(digitos.charAt(0) + "")) { - return false; - } - tamanho = tamanho + 1; - numeros = cnpjValue.substring(0, tamanho); - soma = 0; - pos = tamanho - 7; - for (int i = tamanho; i >= 1; i--) { - soma += Integer.parseInt(numeros.charAt(tamanho - i) + "") * pos--; - if (pos < 2) { - pos = 9; - } - } - resultado = soma % 11 < 2 ? 0 : 11 - soma % 11; - if (resultado != Integer.parseInt(digitos.charAt(1) + "")) { - return false; + for (int i = 0; i < tamanhoConsiderado; i++) { + char caractere = cnpj.charAt(i); + + // Regra da Receita Federal: O valor numérico do caractere para o cálculo + // equivale ao seu código na tabela ASCII subtraído de 48. + // Exemplo 1: '0' (ASCII 48) - 48 = 0 + // Exemplo 2: 'A' (ASCII 65) - 48 = 17 + int valorConvertido = caractere - OFFSET_ASCII; + + soma += valorConvertido * pesos[i]; } - return true; + + int resto = soma % DIVISOR_MODULO_11; + + // Regra padrão do Módulo 11: Se o resto for menor que 2, o dígito é 0. + // Caso contrário, o dígito é a diferença entre o divisor (11) e o resto. + return resto < 2 ? 0 : (DIVISOR_MODULO_11 - resto); } } diff --git a/src/main/script/rules/cnpj.ts b/src/main/script/rules/cnpj.ts index b45b353..91037dc 100644 --- a/src/main/script/rules/cnpj.ts +++ b/src/main/script/rules/cnpj.ts @@ -1,65 +1,83 @@ -import { msg } from '..' +import { msg } from ".." -import { length } from './length' +import { length } from "./length" -function cnpjRule(value: string) { - if (!isValid(value)) { - return msg('cnpj', value) - } +// Constantes estruturais do CNPJ +const DIVISOR_MODULO_11 = 11 +const TAMANHO_BASE_CNPJ = 12 + +// A regra oficial diz para extrair 48 do valor ASCII do caractere alfanumérico +const OFFSET_ASCII = 48 + +// Pesos para o cálculo dos Dígitos Verificadores +const PESOS_DV1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] +const PESOS_DV2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] + +export function cnpjRule(value: string) { + // Ignora valores vazios pois existe a regra do "Required" separada + if (value && !isValid(value)) { + return msg("cnpj", value) + } } function isValid(cnpjValue: string) { - if (!cnpjValue) { - return true - } - if (cnpjValue === ('00000000000000') - || cnpjValue === ('11111111111111') - || cnpjValue === ('22222222222222') - || cnpjValue === ('33333333333333') - || cnpjValue === ('44444444444444') - || cnpjValue === ('55555555555555') - || cnpjValue === ('66666666666666') - || cnpjValue === ('77777777777777') - || cnpjValue === ('88888888888888') - || cnpjValue === ('99999999999999') - || cnpjValue.length !== 14) { - return false - } - let tamanho = cnpjValue.length - 2 - let numeros = cnpjValue.substring(0, tamanho) - const digitos = cnpjValue.substring(tamanho) - let soma = 0 - let pos = tamanho - 7 - for (let i = tamanho; i >= 1; i--) { - soma += parseInt(numeros.charAt(tamanho - i)) * pos-- - if (pos < 2) { - pos = 9 - } - } - let resultado = soma % 11 < 2 ? 0 : 11 - soma % 11 - if (resultado !== parseInt(digitos.charAt(0))) { - return false - } - - tamanho = tamanho + 1 - numeros = cnpjValue.substring(0, tamanho) - soma = 0 - pos = tamanho - 7 - for (let i = tamanho; i >= 1; i--) { - soma += parseInt(numeros.charAt(tamanho - i)) * pos-- - if (pos < 2) { - pos = 9 - } - } - resultado = soma % 11 < 2 ? 0 : 11 - soma % 11 - if (resultado !== parseInt(digitos.charAt(1))) { - return false - } - - return true + if (cnpjValue?.length !== 14) return false + + // 1. Limpeza e Normalização + const cnpjLimpo = cnpjValue.replace(/[^a-zA-Z\d]/g, "").toUpperCase() + + // 2. Validação Estrutural (Regex) + if (!/^[A-Z\d]{12}\d{2}$/.test(cnpjLimpo)) { + return false + } + + // 3. Extração dos dígitos informados no final da string para validação + const dv1Informado = Number.parseInt(cnpjLimpo.charAt(12), 10) + const dv2Informado = Number.parseInt(cnpjLimpo.charAt(13), 10) + + // 4. Cálculo dos dígitos esperados + const dv1Calculado = calcularDigitoVerificador( + cnpjLimpo, + PESOS_DV1, + TAMANHO_BASE_CNPJ, + ) + const dv2Calculado = calcularDigitoVerificador( + cnpjLimpo, + PESOS_DV2, + TAMANHO_BASE_CNPJ + 1, + ) + + // 5. O CNPJ só é válido se ambos os dígitos baterem + return dv1Calculado === dv1Informado && dv2Calculado === dv2Informado +} + +/** + * Calcula o dígito verificador aplicando o Módulo 11 com a regra alfanumérica. + */ +function calcularDigitoVerificador( + cnpj: string, + pesos: number[], + tamanhoConsiderado: number, +): number { + let soma = 0 + + for (let i = 0; i < tamanhoConsiderado; i++) { + const codigoAscii = cnpj.charCodeAt(i) + + // Regra da Receita Federal: O valor numérico do caractere para o cálculo + // equivale ao seu código na tabela ASCII subtraído de 48. + // Exemplo 1: '0' (ASCII 48) - 48 = 0 + // Exemplo 2: 'A' (ASCII 65) - 48 = 17 + const valorConvertido = codigoAscii - OFFSET_ASCII + + soma += valorConvertido * pesos[i] + } + + const resto = soma % DIVISOR_MODULO_11 + + // Regra padrão do Módulo 11: Se o resto for menor que 2, o dígito é 0. + // Caso contrário, o dígito é a diferença entre o divisor (11) e o resto. + return resto < 2 ? 0 : DIVISOR_MODULO_11 - resto } -export const cnpj = [ - length(14), - cnpjRule, -] +export const cnpj = [length(14), cnpjRule] diff --git a/src/test/java/br/ufsc/bridge/platform/validation/rules/CnpjTest.java b/src/test/java/br/ufsc/bridge/platform/validation/rules/CnpjTest.java index a028036..4edead0 100644 --- a/src/test/java/br/ufsc/bridge/platform/validation/rules/CnpjTest.java +++ b/src/test/java/br/ufsc/bridge/platform/validation/rules/CnpjTest.java @@ -10,46 +10,58 @@ public class CnpjTest extends ValidationTest { @Test public void testarRequired() { Assert.assertNull("Cnpj não deve ser requerido por padrão", this.validate(null, Rules.cnpj)); - Assert.assertNull("Cnpj não deve ser requerido por padrão", this.validate("", Rules.cnpj)); } @Test - public void testarValorDeCnpj() { - Assert.assertNull("23348194000116 é um Cnpj válido", this.validate("23348194000116", Rules.cnpj)); - - Assert.assertNull("10781193000119 é um Cnpj válido", this.validate("10781193000119", Rules.cnpj)); - - Assert.assertNull("78471826000126 é um Cnpj válido", this.validate("78471826000126", Rules.cnpj)); + public void testarCnpjNumericoValido() { + Assert.assertNull("23348194000116 é um Cnpj numérico válido", this.validate("23348194000116", Rules.cnpj)); + Assert.assertNull("10781193000119 é um Cnpj numérico válido", this.validate("10781193000119", Rules.cnpj)); + Assert.assertNull("78471826000126 é um Cnpj numérico válido", this.validate("78471826000126", Rules.cnpj)); + Assert.assertNull("86462814000163 é um Cnpj numérico válido", this.validate("86462814000163", Rules.cnpj)); + Assert.assertNull("53110873000113 é um Cnpj numérico válido", this.validate("53110873000113", Rules.cnpj)); + } - Assert.assertNull("86462814000163 é um Cnpj válido", this.validate("86462814000163", Rules.cnpj)); + @Test + public void testarCnpjAlfanumericoValido() { + Assert.assertNull("12ABCDEF345621 é um Cnpj alfanumérico válido", this.validate("12ABCDEF345621", Rules.cnpj)); + Assert.assertNull("A1B2C3D4E5F668 é um Cnpj alfanumérico válido", this.validate("A1B2C3D4E5F668", Rules.cnpj)); + } + @Test + public void testarCnpjInvalido() { + // Numéricos Inválidos (DVs incorretos ou repetidos) Assert.assertNotNull("32432423424234 não é um Cnpj válido", this.validate("32432423424234", Rules.cnpj)); - Assert.assertNotNull("92876904876903 não é um Cnpj válido", this.validate("92876904876903", Rules.cnpj)); - Assert.assertNotNull("85478704024331 não é um Cnpj válido", this.validate("85478704024331", Rules.cnpj)); - Assert.assertNotNull("03216987964642 não é um Cnpj válido", this.validate("03216987964642", Rules.cnpj)); + Assert.assertNotNull("11111111111111 não é um Cnpj válido (Dígitos repetidos)", this.validate("11111111111111", Rules.cnpj)); + Assert.assertNotNull("32758217350712 não é um Cnpj válido", this.validate("32758217350712", Rules.cnpj)); - Assert.assertNotNull("11111111111111 não é um Cnpj válido", this.validate("11111111111111", Rules.cnpj)); + // Alfanuméricos Inválidos (DVs calculados incorretamente) + Assert.assertNotNull("12ABCDEF345600 não é um Cnpj válido (DVs não batem)", this.validate("12ABCDEF345600", Rules.cnpj)); + Assert.assertNotNull("A1B2C3D4E5F600 não é um Cnpj válido (DVs não batem)", this.validate("A1B2C3D4E5F600", Rules.cnpj)); + } - Assert.assertNotNull("32758217350712 não é um Cnpj válido", this.validate("32758217350712", Rules.cnpj)); + @Test + public void testarCaracteresNaoPermitidos() { + Assert.assertNotNull("Os Dígitos Verificadores finais devem ser obrigatoriamente números", this.validate("12ABCDEF3456AB", Rules.cnpj)); - Assert.assertNotNull("No Cnpj são aceitos apenas dígitos", this.validate("23.348.194/0001-16", Rules.cnpj)); + Assert.assertNotNull("Não deve aceitar símbolos ou caracteres especiais (#, $)", this.validate("#123456789$056", Rules.cnpj)); + Assert.assertNotNull("Não deve aceitar símbolos comuns (@)", this.validate("12@BCDEF345621", Rules.cnpj)); - Assert.assertNotNull("No Cnpj são aceitos apenas dígitos", this.validate("012abcdefg9539", Rules.cnpj)); + Assert.assertNotNull("Não deve aceitar caracteres com acentuação", this.validate("12ÁBCDEF345621", Rules.cnpj)); - Assert.assertNotNull("No Cnpj são aceitos apenas dígitos", this.validate("#123456789$056", Rules.cnpj)); + Assert.assertNotNull("Não deve aceitar formatação/pontuação", this.validate("23.348.194/0001-16", Rules.cnpj)); } @Test public void testarTamanho() { Assert.assertNotNull("Cnpj com menos de 14 dígitos não deve ser aceito", this.validate("0123456", Rules.cnpj)); + Assert.assertNotNull("Cnpj alfanumérico com menos de 14 dígitos não deve ser aceito", this.validate("12ABCDEF34562", Rules.cnpj)); Assert.assertNotNull("Cnpj com mais de 14 dígitos não deve ser aceito", this.validate("01234567890123456", Rules.cnpj)); - - Assert.assertNull("Cnpj com 14 dígitos deve ser aceito", this.validate("53110873000113", Rules.cnpj)); + Assert.assertNotNull("Cnpj alfanumérico com mais de 14 dígitos não deve ser aceito", this.validate("12ABCDEF3456210", Rules.cnpj)); } }