diff --git a/src/validator-docs/Rules/Cnpj.php b/src/validator-docs/Rules/Cnpj.php index a3e940a..4375204 100644 --- a/src/validator-docs/Rules/Cnpj.php +++ b/src/validator-docs/Rules/Cnpj.php @@ -4,39 +4,70 @@ namespace geekcom\ValidatorDocs\Rules; -use function mb_strlen; use function preg_match; final class Cnpj extends Sanitization { + private const CNPJ_BASE_LENGTH = 12; + private const REGEX_CNPJ_BASE = '/^[A-Z\d]{12}$/'; + private const REGEX_CNPJ_FULL = '/^[A-Z\d]{12}\d{2}$/'; + private const ASCII_ZERO = 48; // '0' in ASCII + private const ZEROED_CNPJ = '00000000000000'; + + // Weights from right to left: 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5 (for DV1) + // and 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6 (for DV2) + private const DV1_WEIGHTS = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; + private const DV2_WEIGHTS = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; + public function validateCnpj($attribute, $value): bool { - $c = $this->sanitize($value); + $cnpjClean = strtoupper($this->sanitizeCNPJAlphanumeric($value)); + if ($cnpjClean !== self::ZEROED_CNPJ && preg_match(self::REGEX_CNPJ_FULL, $cnpjClean)) { + $providedDV = substr($cnpjClean, self::CNPJ_BASE_LENGTH); + $calculatedDV = $this->calculateCNPJCheckDigits(substr($cnpjClean, 0, self::CNPJ_BASE_LENGTH)); - if (mb_strlen($c) != 14 || preg_match("/^{$c[0]}{14}$/", $c)) { - return false; + return $providedDV === $calculatedDV; } - $b = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; + return false; + } - for ( - $i = 0, $n = 0; $i < 12; $n += $c[$i] * $b[++$i] - ) { - } + /** + * Calculate CNPJ check digits (DV) using the new alphanumeric rules. + */ + public function calculateCNPJCheckDigits($cnpj): string + { + $cnpjClean = strtoupper($this->sanitizeCNPJAlphanumeric($cnpj)); + if (preg_match(self::REGEX_CNPJ_BASE, $cnpjClean) && substr(self::ZEROED_CNPJ, 0, strlen($cnpjClean)) !== $cnpjClean) { + // Calculate DV1 + $sumDV1 = 0; + for ($i = 0; $i < self::CNPJ_BASE_LENGTH; $i++) { + $charValue = ord($cnpjClean[$i]) - self::ASCII_ZERO; + $sumDV1 += $charValue * self::DV1_WEIGHTS[$i]; + } - if ($c[12] != ((($n %= 11) < 2) ? 0 : 11 - $n)) { - return false; - } + $remainder1 = $sumDV1 % 11; + $dv1 = ($remainder1 < 2) ? 0 : 11 - $remainder1; - for ( - $i = 0, $n = 0; $i <= 12; $n += $c[$i] * $b[$i++] - ) { - } + // Calculate DV2 (includes DV1 at the end) + $sumDV2 = 0; + for ($i = 0; $i < self::CNPJ_BASE_LENGTH; $i++) { + $charValue = ord($cnpjClean[$i]) - self::ASCII_ZERO; + $sumDV2 += $charValue * self::DV2_WEIGHTS[$i]; + } + $sumDV2 += $dv1 * self::DV2_WEIGHTS[self::CNPJ_BASE_LENGTH]; + + $remainder2 = $sumDV2 % 11; + $dv2 = ($remainder2 < 2) ? 0 : 11 - $remainder2; - if ($c[13] != ((($n %= 11) < 2) ? 0 : 11 - $n)) { - return false; + return "{$dv1}{$dv2}"; } - return true; + throw new \InvalidArgumentException('Invalid CNPJ'); + } + + private function sanitizeCNPJAlphanumeric(string $value): string + { + return preg_replace('/[.\-\/]/', '', strtoupper(trim($value))); } } diff --git a/tests/TestValidator.php b/tests/TestValidator.php index 2a870b4..7f92453 100644 --- a/tests/TestValidator.php +++ b/tests/TestValidator.php @@ -24,22 +24,35 @@ public function cpf() $this->assertTrue($incorrect->fails()); } - /** @test **/ - public function cnpj() + /** + * @test + * @dataProvider cnpjDataProvider + **/ + public function cnpj(string $cnpj, bool $expected): void { - $correct = Validator::make( - ['certo' => '53.084.587/0001-20'], - ['certo' => 'cnpj'] - ); - - $incorrect = Validator::make( - ['errado' => '51.084.587/0001-20'], - ['errado' => 'cnpj'] - ); + $valid = Validator::make(['cnpj_attribute' => $cnpj], ['cnpj_attribute' => 'cnpj'])->passes(); - $this->assertTrue($correct->passes()); + $this->assertSame($expected, $valid); + } - $this->assertTrue($incorrect->fails()); + public function cnpjDataProvider(): array + { + return [ + 'Alphanumeric CNPJ with dots and dashes 1' => ['T6.JSP.XPS/0001-11', true], + 'Alphanumeric CNPJ with dots and dashes 2' => ['T6.JSP.XPS/J84K-69', true], + 'Alphanumeric CNPJ with dots and dashes 3' => ['D2.M97.AA0/0001-63', true], + 'Alphanumeric CNPJ with dots and dashes 4' => ['W9.7VY.JMY/0001-81', true], + 'Alphanumeric CNPJ clean 1' => ['E5SGVHX9000190', true], + 'Alphanumeric CNPJ clean 2' => ['E5SGVHX960LR53', true], + 'Alphanumeric CNPJ clean 3' => ['12ABC34501DE35', true], + 'Numeric CNPJ with dots and dashes 1' => ['32.332.643/0001-29', true], + 'Numeric CNPJ with dots and dashes 2' => ['32.332.643/3060-40', true], + 'Numeric CNPJ clean 1' => ['38725833000192', true], + 'Numeric CNPJ clean 2' => ['38725833223060', true], + 'Invalid CNPJ' => ['12.345.678/0001-00', false], + 'Zeroed CNPJ' => ['00.000.000/0000-00', false], + 'CNPJ with invalid characters' => ['32.332.643/0001-9!', false], + ]; } /** @test **/