Skip to content

isValidCnpj rejects lowercase alphanumeric CNPJs, but cleanCnpj explicitly uppercases them #500

Description

@enzozon

Description

isValidCnpj(cnpj, { version: 2 }) rejects lowercase alphanumeric CNPJs, even though cleanCnpj contains explicit code to uppercase them.

Reproduction

import { isValidCnpj } from "@brazilian-utils/brazilian-utils";

isValidCnpj("12ABC34501DE35",     { version: 2 }); // true  -> ok
isValidCnpj("12.ABC.345/01DE-35", { version: 2 }); // true  -> ok
isValidCnpj("12abc34501de35",     { version: 2 }); // false -> expected true
isValidCnpj("12.abc.345/01de-35", { version: 2 }); // false -> expected true

12.ABC.345/01DE-35 is the example published by Receita Federal in IN RFB 2.229/2024.

Root cause

In src/is-valid-cnpj/is-valid-cnpj.ts, cleanCnpj goes out of its way to normalise lowercase letters to uppercase:

result += char >= "a" && char <= "z" ? String.fromCharCode(char.charCodeAt(0) - 32) : char;

and the checksum computed from that cleaned value is correct:

cleanCnpj("12abc34501de35")        === "12ABC34501DE35"  // ok
isValidChecksum("12ABC34501DE35")  === true              // ok

But the final guard runs the format regex against the original input rather than the cleaned one:

return hasLetter && FORMAT_REGEX.test(cnpj) && isValidChecksum(cleaned);
//                  ^^^^^^^^^^^^^^^^^^^^^^ original, not `cleaned`

and FORMAT_REGEX has neither the i flag nor a lowercase range:

const FORMAT_REGEX = /^[0-9A-Z]{2}\.?[0-9A-Z]{3}\.?[0-9A-Z]{3}\/?[0-9A-Z]{4}-?[0-9]{2}$/;

So the lowercase branch inside cleanCnpj is effectively dead code on the alphanumeric path: any input that needs it is rejected downstream, before the cleaned value is used for anything other than the checksum.

The numeric path is unaffected -- NUMERIC_FORMAT_REGEX is digits-only, so case never comes up there.

Why tests don't catch it

The round-trip test drives the validator with generateCnpj(version), which only emits uppercase, so lowercase input is never exercised.

Which behaviour is intended?

The two pieces of code disagree, so I'd rather ask before sending a PR:

  1. Accept lowercase (what cleanCnpj implies) -- add the i flag to FORMAT_REGEX.
  2. Reject lowercase (RFB writes the alphanumeric CNPJ in uppercase) -- then the a-z handling in cleanCnpj is dead and should be removed, so the intent is explicit.

Happy to open a PR with a regression test for whichever you prefer.

Unrelated to #498, which touches generate-cnpj only.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions