Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DanfeSharp.Test/DanfeSharp.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<Compile Include="DanfeXmlTests.cs" />
<Compile Include="Extentions.cs" />
<Compile Include="FabricaFake.cs" />
<Compile Include="FormatadorTests.cs" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Importante — estes testes não rodam via dotnet test. O assembly DanfeSharp.Test não compila: DanfeTest.cs:63 referencia DanfeCCC, classe que não existe no repo (confirmei). Enquanto o test project não buildar, os 36 casos deste arquivo não executam em CI nem localmente — só foram validados por dotnet-script, que exercita o Formatador mas não o arquivo de teste em si.

💡 Recomendo corrigir o test project neste PR (ou em PR pré-requisito mergeado antes deste). Caso contrário, são testes mortos até o follow-up — e o risco é que nunca rodem e a regex sofra regressão silenciosa.

<Compile Include="LogoTests.cs" />
<Compile Include="SimplePdfPageTester.cs" />
<Compile Include="DanfeTest.cs" />
Expand Down
116 changes: 116 additions & 0 deletions DanfeSharp.Test/FormatadorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DanfeSharp.Test
{
/// <summary>
/// Cobertura do <see cref="Formatador"/> para CNPJ alfanumérico (Reforma
/// Tributária — LC 214/2025, vigência 2026-07-01) e regressão dos formatos
/// numéricos legados.
/// </summary>
[TestClass]
public class FormatadorTests
{
// CNPJ alfanumérico: regex agora aceita [0-9A-Z]{12}\d{2}. Máscara
// visual `XX.XXX.XXX/XXXX-XX` aplicada com letras nas posições corretas.

[DataTestMethod]
[DataRow("12345678000190", "12.345.678/0001-90", DisplayName = "CNPJ numérico legado")]
[DataRow("11222333000181", "11.222.333/0001-81", DisplayName = "CNPJ numérico válido")]
[DataRow("12ABC345000188", "12.ABC.345/0001-88", DisplayName = "CNPJ alfanumérico letras no meio")]
[DataRow("98XYZ123000158", "98.XYZ.123/0001-58", DisplayName = "CNPJ alfanumérico outro vetor")]
public void FormatarCnpj_FormatosValidos_AplicaMascara(string input, string expected)
{
Assert.AreEqual(expected, Formatador.FormatarCnpj(input));
}

[DataTestMethod]
[DataRow("12.345.678/0001-90", "12.345.678/0001-90", DisplayName = "CNPJ já formatado (idempotência)")]
[DataRow("12.ABC.345/0001-88", "12.ABC.345/0001-88", DisplayName = "CNPJ alfanumérico já formatado")]
public void FormatarCnpj_JaFormatado_PreservaMascara(string input, string expected)
{
Assert.AreEqual(expected, Formatador.FormatarCnpj(input));
}

[DataTestMethod]
[DataRow("12345678", DisplayName = "Muito curto")]
[DataRow("123456789012345", DisplayName = "Muito longo")]
[DataRow("12abc345000188", DisplayName = "Letras minúsculas — rejeitadas")]
[DataRow("12ABC345000ABCD", DisplayName = "DV alfanumérico — rejeitado")]
public void FormatarCnpj_ComprimentoOuFormatoInvalido_RetornaInputSemMascara(string input)
{
// Comportamento histórico: quando a regex não bate, o input é devolvido
// sem máscara, sem exception.
Assert.AreEqual(input, Formatador.FormatarCnpj(input));
}

[DataTestMethod]
[DataRow("12345678901", "123.456.789-01")]
[DataRow("11144477735", "111.444.777-35")]
[DataRow("123.456.789-01", "123.456.789-01")]
public void FormatarCpf_FormatosValidos_AplicaMascara(string input, string expected)
{
Assert.AreEqual(expected, Formatador.FormatarCpf(input));
}

[DataTestMethod]
[DataRow("1234567890A", DisplayName = "CPF com letra — rejeitado (CPF nunca aceita alfanumérico)")]
[DataRow("123", DisplayName = "Muito curto")]
[DataRow("123456789012", DisplayName = "Muito longo")]
public void FormatarCpf_FormatoInvalido_RetornaInputSemMascara(string input)
{
Assert.AreEqual(input, Formatador.FormatarCpf(input));
}

// FormatarCpfCnpj decide por comprimento da string limpa (sem máscara)
// antes da regex — eliminando ambiguidade quando um CNPJ alfanumérico
// pudesse acidentalmente bater na regex CPF (improvável mas defensivo).

[DataTestMethod]
[DataRow("12345678901", "123.456.789-01", DisplayName = "CPF numérico")]
[DataRow("11144477735", "111.444.777-35", DisplayName = "CPF numérico vetor 2")]
[DataRow("12345678000190", "12.345.678/0001-90", DisplayName = "CNPJ numérico legado")]
[DataRow("12ABC345000188", "12.ABC.345/0001-88", DisplayName = "CNPJ alfanumérico")]
[DataRow("98XYZ123000158", "98.XYZ.123/0001-58", DisplayName = "CNPJ alfanumérico vetor 2")]
public void FormatarCpfCnpj_FormatosValidos_AplicaMascaraCorrespondente(string input, string expected)
{
Assert.AreEqual(expected, Formatador.FormatarCpfCnpj(input));
}

[DataTestMethod]
[DataRow("123.456.789-01", "123.456.789-01", DisplayName = "CPF com máscara — limpa e refaz")]
[DataRow("12.345.678/0001-90", "12.345.678/0001-90", DisplayName = "CNPJ com máscara — limpa e refaz")]
[DataRow("12.ABC.345/0001-88", "12.ABC.345/0001-88", DisplayName = "CNPJ alfanumérico com máscara — limpa e refaz")]
public void FormatarCpfCnpj_ComMascara_NormalizaEReaplica(string input, string expected)
{
Assert.AreEqual(expected, Formatador.FormatarCpfCnpj(input));
}

[DataTestMethod]
[DataRow("123", DisplayName = "3 chars — não é CPF nem CNPJ")]
[DataRow("123456789012", DisplayName = "12 chars — comprimento inválido")]
[DataRow("1234567890123", DisplayName = "13 chars — comprimento inválido")]
public void FormatarCpfCnpj_ComprimentoInvalido_RetornaInputTrimadoSemMascara(string input)
{
// Não joga exception (comportamento histórico). Apenas devolve trimado.
Assert.AreEqual(input, Formatador.FormatarCpfCnpj(input));
}

[TestMethod]
public void FormatarCpfCnpj_StringVazia_RetornaStringVazia()
{
Assert.AreEqual(string.Empty, Formatador.FormatarCpfCnpj(string.Empty));
}

[TestMethod]
public void FormatarCpfCnpj_Null_RetornaStringVazia()
{
Assert.AreEqual(string.Empty, Formatador.FormatarCpfCnpj(null));
}

[TestMethod]
public void FormatarCpfCnpj_StringComEspacos_TrimaAntesDeFormatar()
{
Assert.AreEqual("12.ABC.345/0001-88", Formatador.FormatarCpfCnpj(" 12ABC345000188 "));
}
}
}
46 changes: 25 additions & 21 deletions DanfeSharp/Formatador.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ static Formatador()
public const String FormatoNumeroNF = @"000\.000\.000";

public const String CEP = @"^(\d{5})\-?(\d{3})$";
public const String CNPJ = @"^(\d{2})\.?(\d{3})\.?(\d{3})\/?(\d{4})\-?(\d{2})$";
// CNPJ aceita letras maiúsculas nos 12 primeiros caracteres a partir de 2026-07-01
// (Reforma Tributária, LC 214/2025 e IN RFB 2.229/2024). Os 2 últimos seguem como
// dígitos verificadores numéricos. CPF permanece estritamente numérico.
public const String CNPJ = @"^([0-9A-Z]{2})\.?([0-9A-Z]{3})\.?([0-9A-Z]{3})\/?([0-9A-Z]{4})\-?(\d{2})$";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Regex correta conforme a norma: 12 primeiros [0-9A-Z], DV final \d{2} numérico.

💡 Decisão de design a confirmar: lowercase é rejeitado (volta sem máscara) em vez de normalizado para maiúsculo. É defensável — a forma canônica SEFAZ é maiúscula — e os testes assertam isso explicitamente. Só vale garantir que nenhum caminho upstream entregue o CNPJ em minúsculas ao formatador; se entregar, o DANFE renderiza sem máscara silenciosamente. Se houver essa dúvida, um .ToUpperInvariant() no clean resolveria de forma conservadora.

public const String CPF = @"^(\d{3})\.?(\d{3})\.?(\d{3})\-?(\d{2})$";
public const String Telefone = @"^\(?(\d{2})\)?\s*(\d{4,5})\s*\-?\s*(\d{4})$";
public const String Placa = @"^([A-Z]{3})\s*\-?\s*(\d{4})$";
Expand Down Expand Up @@ -122,33 +125,34 @@ public static String FormatarCpf(String cpf)
}

/// <summary>
/// Formata um número de documento
/// Formata um número de documento (CPF ou CNPJ).
/// Decide pelo comprimento da string limpa (sem máscara) antes de aplicar regex,
/// eliminando ambiguidade entre os formatos — relevante a partir de 2026-07-01,
/// quando o CNPJ passa a aceitar letras nos 12 primeiros caracteres.
/// </summary>
/// <param name="cpfCnpj"></param>
/// <returns></returns>
/// <param name="cpfCnpj">Documento com ou sem máscara visual.</param>
/// <returns>Documento formatado, ou o input trimado se comprimento for inválido.</returns>
public static String FormatarCpfCnpj(String cpfCnpj)
{
String result;

if (!String.IsNullOrWhiteSpace(cpfCnpj))
if (String.IsNullOrWhiteSpace(cpfCnpj))
{
result = cpfCnpj.Trim();

if (Regex.IsMatch(result, CPF))
{
result = FormatarCpf(result);
}
else if (Regex.IsMatch(result, CNPJ))
{
result = FormatarCnpj(result);
}
return String.Empty;
}
else

String trimmed = cpfCnpj.Trim();
String clean = trimmed.Replace(".", String.Empty).Replace("-", String.Empty).Replace("/", String.Empty);

switch (clean.Length)
{
result = String.Empty;
case 11:
return FormatarCpf(clean);
case 14:
return FormatarCnpj(clean);
Comment thread
rhfranzoni marked this conversation as resolved.
Outdated
default:
// Comprimento inválido — devolve sem aplicar máscara (comportamento
// alinhado com o histórico: nunca jogou exception).
return trimmed;
}

return result;
}

/// <summary>
Expand Down