diff --git a/DanfeSharp.Test/DanfeSharp.Test.csproj b/DanfeSharp.Test/DanfeSharp.Test.csproj
index 353f536..d3fbd8d 100644
--- a/DanfeSharp.Test/DanfeSharp.Test.csproj
+++ b/DanfeSharp.Test/DanfeSharp.Test.csproj
@@ -63,6 +63,7 @@
+
diff --git a/DanfeSharp.Test/DanfeSimplificadoTipo2Tests.cs b/DanfeSharp.Test/DanfeSimplificadoTipo2Tests.cs
new file mode 100644
index 0000000..ccb1fb6
--- /dev/null
+++ b/DanfeSharp.Test/DanfeSimplificadoTipo2Tests.cs
@@ -0,0 +1,421 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using DanfeSharp.Esquemas.NFe;
+using DanfeSharp.Modelo;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using org.pdfclown.tools;
+using PdfFile = org.pdfclown.files.File;
+
+namespace DanfeSharp.Test
+{
+ ///
+ /// Testes do DANFE Simplificado Tipo 2 (NT 2026.003, tpImp=6) — 9 divisões
+ /// renderizadas pela classe DanfeSimplificadoTipo2.
+ /// See openspec/specs/danfe-simplificado-tipo-2/spec.md
+ ///
+ [TestClass]
+ public class DanfeSimplificadoTipo2Tests
+ {
+ public readonly string OutputDirectory = Path.Combine("Output", "SimplificadoTipo2");
+
+ public DanfeSimplificadoTipo2Tests()
+ {
+ if (!Directory.Exists(OutputDirectory))
+ Directory.CreateDirectory(OutputDirectory);
+ }
+
+ private static DanfeViewModel CriarViewModelTipo2()
+ {
+ var m = new DanfeViewModel
+ {
+ NfNumero = 123456,
+ NfSerie = 1,
+ ChaveAcesso = "35260607952851000109550010001234561000123459",
+ TipoAmbiente = 1,
+ TipoEmissao = FormaEmissao.Normal,
+ ProtocoloAutorizacao = "135260000012345 01/07/2026 10:30:00",
+ DataHoraEmissao = new DateTime(2026, 7, 1, 10, 15, 0),
+ EndConsulta = "SP".UrlNFeConsulta(1),
+ Emitente = new EmpresaViewModel
+ {
+ CnpjCpf = "07952851000109",
+ RazaoSocial = "Mercado Tupã Ltda",
+ EnderecoLogadrouro = "Avenida Brasil",
+ EnderecoNumero = "1000",
+ EnderecoBairro = "Centro",
+ EnderecoUf = "SP",
+ Municipio = "São Paulo"
+ },
+ Destinatario = new EmpresaViewModel
+ {
+ CnpjCpf = "12345678909",
+ RazaoSocial = "João da Silva",
+ EnderecoLogadrouro = "Rua das Flores",
+ EnderecoNumero = "42",
+ EnderecoBairro = "Jardim",
+ EnderecoUf = "SP",
+ Municipio = "São Paulo"
+ },
+ Produtos = new List
+ {
+ new ProdutoViewModel
+ {
+ Codigo = "001",
+ Descricao = "Arroz Tipo 1 5kg",
+ Quantidade = 2,
+ Unidade = "UN",
+ ValorUnitario = 25.50,
+ ValorTotal = 51.00
+ }
+ },
+ Pagamento = new List
+ {
+ new PagamentoViewModel
+ {
+ Troco = 9.00m,
+ DetalhePagamento = new List
+ {
+ new DetalheViewModel { FormaPagamento = FormaPagamento.fpDinheiro, Valor = 60.00m }
+ }
+ }
+ }
+ };
+
+ m.CalculoImposto.QuantidadeTotal = 2;
+ m.CalculoImposto.ValorTotalProdutos = 51.00;
+ m.CalculoImposto.ValorTotalNota = 51.00;
+
+ return m;
+ }
+
+ private string GerarPdfETexto(DanfeViewModel model, string outFileName)
+ {
+ var pdfPath = Path.Combine(OutputDirectory, outFileName);
+ using (var danfe = new DanfeSimplificadoTipo2(model))
+ {
+ danfe.Gerar();
+ danfe.Salvar(pdfPath);
+ }
+ return pdfPath;
+ }
+
+ private static string ExtrairTextoPdf(string pdfPath)
+ {
+ var sb = new StringBuilder();
+ using (var file = new PdfFile(pdfPath))
+ {
+ var extractor = new TextExtractor();
+ foreach (var page in file.Document.Pages)
+ {
+ var areaTextStrings = extractor.Extract(page);
+ foreach (var pair in areaTextStrings)
+ {
+ foreach (var textString in pair.Value)
+ {
+ sb.AppendLine(textString.Text);
+ }
+ }
+ }
+ }
+ return sb.ToString();
+ }
+
+ private static int ContarPaginas(string pdfPath)
+ {
+ using (var file = new PdfFile(pdfPath))
+ {
+ return file.Document.Pages.Count;
+ }
+ }
+
+ // === Divisão I — Cabeçalho ===
+
+ [TestMethod]
+ public void DivisaoI_CabecalhoContemTituloEmitenteECnpj()
+ {
+ var texto = ExtrairTextoPdf(GerarPdfETexto(CriarViewModelTipo2(), "DivisaoI_Cabecalho.pdf"));
+
+ StringAssert.Contains(texto, "DANFE Simplificado Tipo 2");
+ StringAssert.Contains(texto, "Mercado Tupã Ltda");
+ StringAssert.Contains(texto, "07.952.851/0001-09");
+ }
+
+ // === Divisões II e III — Produtos e Totais ===
+
+ [TestMethod]
+ public void DivisaoII_ProdutoRenderizado()
+ {
+ var texto = ExtrairTextoPdf(GerarPdfETexto(CriarViewModelTipo2(), "DivisaoII_Produtos.pdf"));
+
+ StringAssert.Contains(texto, "Arroz Tipo 1 5kg");
+ StringAssert.Contains(texto, "QTD. TOTAL DE ITENS");
+ }
+
+ [TestMethod]
+ public void DivisaoIII_TrocoSemprePresente_MesmoSemValor()
+ {
+ var model = CriarViewModelTipo2();
+ model.Pagamento[0].Troco = null;
+
+ var texto = ExtrairTextoPdf(GerarPdfETexto(model, "DivisaoIII_TrocoZerado.pdf"));
+
+ StringAssert.Contains(texto, "TROCO");
+ StringAssert.Contains(texto, "VALOR PAGO");
+ }
+
+ [TestMethod]
+ public void DivisaoIII_FormaPagamentoEValores()
+ {
+ var texto = ExtrairTextoPdf(GerarPdfETexto(CriarViewModelTipo2(), "DivisaoIII_Pagamento.pdf"));
+
+ StringAssert.Contains(texto, "Dinheiro");
+ StringAssert.Contains(texto, "60,00");
+ StringAssert.Contains(texto, "9,00");
+ }
+
+ // === Divisão III-A — IBS/CBS/IS ===
+
+ [TestMethod]
+ public void DivisaoIIIA_ComGrupoIbsCbs_RenderizaRotulosEValores()
+ {
+ var model = CriarViewModelTipo2();
+ model.TributosIbsCbs = new TributosIbsCbsViewModel
+ {
+ BaseCalculo = 51.00m,
+ ValorIbs = 5.10m,
+ ValorIbsUF = 3.06m,
+ ValorIbsMun = 2.04m,
+ ValorCbs = 4.59m
+ };
+
+ var texto = ExtrairTextoPdf(GerarPdfETexto(model, "DivisaoIIIA_ComIbsCbs.pdf"));
+
+ StringAssert.Contains(texto, "TRIBUTOS IBS/CBS");
+ StringAssert.Contains(texto, "IBS");
+ StringAssert.Contains(texto, "CBS");
+ StringAssert.Contains(texto, "5,10");
+ StringAssert.Contains(texto, "4,59");
+ }
+
+ [TestMethod]
+ public void DivisaoIIIA_SemGrupoIbsCbs_DivisaoOmitida()
+ {
+ var model = CriarViewModelTipo2();
+ model.TributosIbsCbs = null;
+
+ var texto = ExtrairTextoPdf(GerarPdfETexto(model, "DivisaoIIIA_SemIbsCbs.pdf"));
+
+ Assert.IsFalse(texto.Contains("TRIBUTOS IBS/CBS"),
+ "Divisão III-A deve ser omitida quando o XML não tem o grupo IBSCBSTot (fidelidade ao XML).");
+ }
+
+ [TestMethod]
+ public void DivisaoIIIA_ImpostoSeletivo_QuandoPresente()
+ {
+ var model = CriarViewModelTipo2();
+ model.TributosIbsCbs = new TributosIbsCbsViewModel { ValorIS = 1.23m };
+
+ var texto = ExtrairTextoPdf(GerarPdfETexto(model, "DivisaoIIIA_ComIS.pdf"));
+
+ StringAssert.Contains(texto, "IMPOSTO SELETIVO (IS)");
+ }
+
+ // === Divisão IV — Consulta via chave ===
+
+ [TestMethod]
+ public void DivisaoIV_ChaveEmOnzeBlocosDeQuatroDigitos()
+ {
+ var model = CriarViewModelTipo2();
+ var texto = ExtrairTextoPdf(GerarPdfETexto(model, "DivisaoIV_Chave.pdf"));
+
+ StringAssert.Contains(texto, model.ChaveAcesso.SpaceOnAccessKey(),
+ "Chave deve aparecer formatada em 11 blocos de 4 dígitos separados por espaço.");
+ StringAssert.Contains(texto, "CHAVE DE ACESSO");
+ StringAssert.Contains(texto, model.EndConsulta);
+ }
+
+ // === Divisão V — QR Code ===
+
+ [TestMethod]
+ public void DivisaoV_ComQrCode_RenderizaSemExcecao()
+ {
+ var model = CriarViewModelTipo2();
+ model.QrCode = "https://www.nfe.fazenda.gov.br/portal/consultaRecaptcha.aspx?p=35260607952851000109550010001234561000123459|3|1|1|ABCD";
+
+ var texto = ExtrairTextoPdf(GerarPdfETexto(model, "DivisaoV_ComQr.pdf"));
+
+ StringAssert.Contains(texto, "CONSULTA VIA LEITOR DE QR CODE");
+ }
+
+ [TestMethod]
+ public void DivisaoV_SemQrCode_OmiteDivisaoSemExcecao()
+ {
+ var model = CriarViewModelTipo2();
+ model.QrCode = null;
+
+ var texto = ExtrairTextoPdf(GerarPdfETexto(model, "DivisaoV_SemQr.pdf"));
+
+ Assert.IsFalse(texto.Contains("CONSULTA VIA LEITOR DE QR CODE"),
+ "Sem conteúdo de QR o bloco deve ser omitido, sem erro.");
+ }
+
+ // === Divisão VI — Consumidor ===
+
+ [TestMethod]
+ public void DivisaoVI_ConsumidorComCpf()
+ {
+ var texto = ExtrairTextoPdf(GerarPdfETexto(CriarViewModelTipo2(), "DivisaoVI_ConsumidorCpf.pdf"));
+
+ StringAssert.Contains(texto, "CONSUMIDOR CPF:");
+ StringAssert.Contains(texto, "João da Silva");
+ }
+
+ [TestMethod]
+ public void DivisaoVI_ConsumidorComCnpj()
+ {
+ var model = CriarViewModelTipo2();
+ model.Destinatario.CnpjCpf = "07952851000109";
+
+ var texto = ExtrairTextoPdf(GerarPdfETexto(model, "DivisaoVI_ConsumidorCnpj.pdf"));
+
+ StringAssert.Contains(texto, "CONSUMIDOR CNPJ:");
+ }
+
+ [TestMethod]
+ public void DivisaoVI_ConsumidorNaoIdentificado()
+ {
+ var model = CriarViewModelTipo2();
+ model.Destinatario = null;
+
+ var texto = ExtrairTextoPdf(GerarPdfETexto(model, "DivisaoVI_NaoIdentificado.pdf"));
+
+ StringAssert.Contains(texto, "CONSUMIDOR NÃO IDENTIFICADO");
+ }
+
+ // === Divisão VII — Identificação da NF-e ===
+
+ [TestMethod]
+ public void DivisaoVII_NumeroSerieProtocoloEVia()
+ {
+ var texto = ExtrairTextoPdf(GerarPdfETexto(CriarViewModelTipo2(), "DivisaoVII_Identificacao.pdf"));
+
+ StringAssert.Contains(texto, "123456");
+ StringAssert.Contains(texto, "Série: 1");
+ StringAssert.Contains(texto, "PROTOCOLO DE AUTORIZAÇÃO");
+ StringAssert.Contains(texto, "135260000012345");
+ StringAssert.Contains(texto, "Via do Consumidor");
+ }
+
+ // === Divisão VIII — Homologação e Contingência ===
+
+ [TestMethod]
+ public void DivisaoVIII_Homologacao_ExibeMensagemSemValorFiscal()
+ {
+ var model = CriarViewModelTipo2();
+ model.TipoAmbiente = 2;
+
+ var texto = ExtrairTextoPdf(GerarPdfETexto(model, "DivisaoVIII_Homologacao.pdf"));
+
+ StringAssert.Contains(texto, "EMITIDA EM AMBIENTE DE HOMOLOGAÇÃO - SEM VALOR FISCAL");
+ }
+
+ [TestMethod]
+ public void DivisaoVIII_ContingenciaPendente_MensagemEmDoisLocaisESegundaVia()
+ {
+ var model = CriarViewModelTipo2();
+ model.TipoEmissao = FormaEmissao.ContingenciaFSDA;
+ model.ProtocoloAutorizacao = null;
+
+ var path = GerarPdfETexto(model, "DivisaoVIII_Contingencia.pdf");
+ var texto = ExtrairTextoPdf(path);
+
+ Assert.AreEqual(2, ContarPaginas(path), "Contingência pendente deve gerar 2 vias (2 páginas).");
+
+ var ocorrencias = Regex.Matches(texto, "EMITIDA EM CONTINGÊNCIA").Count;
+ Assert.AreEqual(4, ocorrencias, "Mensagem deve aparecer em 2 locais por via × 2 vias = 4 ocorrências.");
+
+ StringAssert.Contains(texto, "PENDENTE DE AUTORIZAÇÃO");
+ StringAssert.Contains(texto, "Via do Consumidor");
+ StringAssert.Contains(texto, "Via do Estabelecimento");
+ }
+
+ [TestMethod]
+ public void DivisaoVIII_EmissaoNormal_SemContingenciaEUmaViaSo()
+ {
+ var path = GerarPdfETexto(CriarViewModelTipo2(), "DivisaoVIII_Normal.pdf");
+ var texto = ExtrairTextoPdf(path);
+
+ Assert.AreEqual(1, ContarPaginas(path), "Emissão normal gera via única.");
+ Assert.IsFalse(texto.Contains("EMITIDA EM CONTINGÊNCIA"));
+ }
+
+ [TestMethod]
+ public void DivisaoVIII_ContingenciaJaAutorizada_NaoExibeMensagemPendente()
+ {
+ // Autorizada após contingência: com protocolo presente não há pendência —
+ // mensagem e segunda via não se aplicam.
+ var model = CriarViewModelTipo2();
+ model.TipoEmissao = FormaEmissao.ContingenciaFSDA;
+
+ var path = GerarPdfETexto(model, "DivisaoVIII_ContingenciaAutorizada.pdf");
+ var texto = ExtrairTextoPdf(path);
+
+ Assert.AreEqual(1, ContarPaginas(path));
+ Assert.IsFalse(texto.Contains("EMITIDA EM CONTINGÊNCIA"));
+ }
+
+ // === Divisão IX — Mensagem do contribuinte ===
+
+ [TestMethod]
+ public void DivisaoIX_Lei12741EInformacoesComplementares()
+ {
+ var model = CriarViewModelTipo2();
+ model.CalculoImposto.ValorAproximadoTributos = 9.18;
+ model.InformacoesComplementares = "Obrigado pela preferência!";
+ model.InformacoesAdicionaisFisco = "Documento emitido conforme NT 2026.003.";
+
+ var texto = ExtrairTextoPdf(GerarPdfETexto(model, "DivisaoIX_Mensagens.pdf"));
+
+ StringAssert.Contains(texto, "LEI 12.741/2012");
+ StringAssert.Contains(texto, "Obrigado pela preferência!");
+ StringAssert.Contains(texto, "Documento emitido conforme NT 2026.003.");
+ }
+
+ // === Smoke de altura dinâmica ===
+
+ [DataTestMethod]
+ [DataRow(1)]
+ [DataRow(30)]
+ [DataRow(200)]
+ public void Smoke_AlturaDinamica_GeraSemExcecao(int quantidadeProdutos)
+ {
+ var model = CriarViewModelTipo2();
+ model.Produtos = new List();
+
+ for (int i = 1; i <= quantidadeProdutos; i++)
+ {
+ model.Produtos.Add(new ProdutoViewModel
+ {
+ Codigo = i.ToString(),
+ Descricao = $"Produto da linha {i}",
+ Quantidade = 1,
+ Unidade = "UN",
+ ValorUnitario = 1.00,
+ ValorTotal = 1.00
+ });
+ }
+
+ var path = GerarPdfETexto(model, $"Smoke_{quantidadeProdutos}itens.pdf");
+
+ Assert.IsTrue(File.Exists(path));
+ Assert.IsTrue(new FileInfo(path).Length > 0);
+
+ var texto = ExtrairTextoPdf(path);
+ StringAssert.Contains(texto, $"Produto da linha {quantidadeProdutos}",
+ "Último produto deve estar dentro da página (altura estimada suficiente).");
+ }
+ }
+}
diff --git a/DanfeSharp/Blocos/SimplificadoTipo2/BlocoCabecalhoSimplificadoT2.cs b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoCabecalhoSimplificadoT2.cs
new file mode 100644
index 0000000..d33602f
--- /dev/null
+++ b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoCabecalhoSimplificadoT2.cs
@@ -0,0 +1,60 @@
+using DanfeSharp.Modelo;
+using org.pdfclown.documents.contents.composition;
+using System.Drawing;
+
+namespace DanfeSharp.Blocos.SimplificadoTipo2
+{
+ ///
+ /// Divisão I (NT 2026.003) — identificação do emitente + título destacado
+ /// "DANFE Simplificado Tipo 2".
+ ///
+ internal class BlocoCabecalhoSimplificadoT2 : ElementoBase
+ {
+ public BlocoCabecalhoSimplificadoT2(DanfeViewModel viewModel, Estilo estilo, PrimitiveComposer primitiveComposer) : base(estilo)
+ {
+ primitiveComposer.BeginLocalState();
+ primitiveComposer.SetFont(estilo.FonteCampoTituloNegrito.FonteInterna, estilo.FonteCampoTituloNegrito.Tamanho);
+
+ primitiveComposer.ShowText("DANFE Simplificado Tipo 2", new PointF(140, 12), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+
+ float y = 22;
+
+ var emitente = viewModel.Emitente;
+ var razaoSocial = emitente.RazaoSocial ?? string.Empty;
+
+ primitiveComposer.SetFont(estilo.FonteCampoConteudoNegrito.FonteInterna, estilo.FonteCampoConteudoNegrito.Tamanho);
+
+ if (razaoSocial.Length > 39)
+ {
+ primitiveComposer.ShowText(razaoSocial.Substring(0, 39), new PointF(140, y), XAlignmentEnum.Center, YAlignmentEnum.Top, 0);
+ y += 10;
+ primitiveComposer.ShowText(razaoSocial.Substring(39), new PointF(140, y), XAlignmentEnum.Center, YAlignmentEnum.Top, 0);
+ }
+ else
+ {
+ primitiveComposer.ShowText(razaoSocial, new PointF(140, y), XAlignmentEnum.Center, YAlignmentEnum.Top, 0);
+ }
+
+ y += 10;
+ primitiveComposer.ShowText($"CNPJ: {Formatador.FormatarCnpj(emitente.CnpjCpf)}", new PointF(140, y), XAlignmentEnum.Center, YAlignmentEnum.Top, 0);
+
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, estilo.FonteCampoConteudo.Tamanho);
+
+ if (!string.IsNullOrWhiteSpace(emitente.EnderecoLogadrouro) &&
+ !string.IsNullOrWhiteSpace(emitente.Municipio))
+ {
+ y += 10;
+ primitiveComposer.ShowText($"{emitente.EnderecoLogadrouro}, {emitente.EnderecoNumero}", new PointF(140, y), XAlignmentEnum.Center, YAlignmentEnum.Top, 0);
+ y += 10;
+ primitiveComposer.ShowText($"{emitente.EnderecoBairro} - {emitente.Municipio} - {emitente.EnderecoUf}", new PointF(140, y), XAlignmentEnum.Center, YAlignmentEnum.Top, 0);
+ }
+
+ Y_NFC = y + 15;
+
+ primitiveComposer.DrawLine(new PointF(15, Y_NFC), new PointF(265, Y_NFC));
+ primitiveComposer.SetLineDash(new org.pdfclown.documents.contents.LineDash(new double[] { 3, 2 }));
+ primitiveComposer.Stroke();
+ primitiveComposer.End();
+ }
+ }
+}
diff --git a/DanfeSharp/Blocos/SimplificadoTipo2/BlocoConsultaChaveT2.cs b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoConsultaChaveT2.cs
new file mode 100644
index 0000000..aa3d42e
--- /dev/null
+++ b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoConsultaChaveT2.cs
@@ -0,0 +1,34 @@
+using DanfeSharp.Modelo;
+using org.pdfclown.documents.contents.composition;
+using System.Drawing;
+
+namespace DanfeSharp.Blocos.SimplificadoTipo2
+{
+ ///
+ /// Divisão IV (NT 2026.003) — consulta pela chave de acesso: URL da SEFAZ e chave
+ /// de 44 dígitos em 11 blocos de 4 dígitos.
+ ///
+ internal class BlocoConsultaChaveT2 : ElementoBase
+ {
+ public BlocoConsultaChaveT2(DanfeViewModel viewModel, Estilo estilo, PrimitiveComposer primitiveComposer, float y) : base(estilo)
+ {
+ primitiveComposer.BeginLocalState();
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, estilo.FonteCampoConteudo.Tamanho);
+ primitiveComposer.ShowText("Consulta pela chave de acesso em", new PointF(140, y + 10), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(viewModel.EndConsulta, new PointF(140, y + 20), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText("CHAVE DE ACESSO", new PointF(140, y + 30), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+
+ // Chave em 11 blocos de 4 dígitos (54 caracteres com espaços) — fonte 7
+ // para caber na largura útil do cupom.
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, 7);
+ primitiveComposer.ShowText(viewModel.ChaveAcesso.SpaceOnAccessKey(), new PointF(140, y + 40), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+
+ Y_NFC = y + 50;
+
+ primitiveComposer.DrawLine(new PointF(15, Y_NFC), new PointF(265, Y_NFC));
+ primitiveComposer.SetLineDash(new org.pdfclown.documents.contents.LineDash(new double[] { 3, 2 }));
+ primitiveComposer.Stroke();
+ primitiveComposer.End();
+ }
+ }
+}
diff --git a/DanfeSharp/Blocos/SimplificadoTipo2/BlocoConsumidorT2.cs b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoConsumidorT2.cs
new file mode 100644
index 0000000..2d1c680
--- /dev/null
+++ b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoConsumidorT2.cs
@@ -0,0 +1,58 @@
+using DanfeSharp.Modelo;
+using org.pdfclown.documents.contents.composition;
+using System.Drawing;
+
+namespace DanfeSharp.Blocos.SimplificadoTipo2
+{
+ ///
+ /// Divisão VI (NT 2026.003) — identificação do consumidor: "CONSUMIDOR CNPJ:" ou
+ /// "CONSUMIDOR CPF:" com nome e endereço quando presentes no XML. A obrigatoriedade
+ /// em operação não presencial é validada na emissão (domínio), não aqui.
+ ///
+ internal class BlocoConsumidorT2 : ElementoBase
+ {
+ public BlocoConsumidorT2(DanfeViewModel viewModel, Estilo estilo, PrimitiveComposer primitiveComposer, float y) : base(estilo)
+ {
+ primitiveComposer.BeginLocalState();
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, estilo.FonteCampoConteudo.Tamanho);
+
+ var dest = viewModel.Destinatario;
+
+ if (dest == null || string.IsNullOrWhiteSpace(dest.CnpjCpf))
+ {
+ Y_NFC = y + 10;
+ primitiveComposer.ShowText("CONSUMIDOR NÃO IDENTIFICADO", new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ }
+ else
+ {
+ var rotuloDocumento = dest.CnpjCpf.Trim().Length == 14 ? "CNPJ" : "CPF";
+
+ Y_NFC = y + 10;
+ primitiveComposer.ShowText($"CONSUMIDOR {rotuloDocumento}: {Formatador.FormatarCpfCnpj(dest.CnpjCpf)}", new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+
+ if (!string.IsNullOrWhiteSpace(dest.RazaoSocial))
+ {
+ Y_NFC += 10;
+ var nome = dest.RazaoSocial.Length > 60 ? dest.RazaoSocial.Substring(0, 60) : dest.RazaoSocial;
+ primitiveComposer.ShowText(nome, new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ }
+
+ if (!string.IsNullOrWhiteSpace(dest.EnderecoLogadrouro) &&
+ !string.IsNullOrWhiteSpace(dest.Municipio))
+ {
+ Y_NFC += 10;
+ primitiveComposer.ShowText($"{dest.EnderecoLogadrouro}, {dest.EnderecoNumero} - {dest.EnderecoBairro}", new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ Y_NFC += 10;
+ primitiveComposer.ShowText($"{dest.Municipio} - {dest.EnderecoUf}", new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ }
+ }
+
+ Y_NFC += 10;
+
+ primitiveComposer.DrawLine(new PointF(15, Y_NFC), new PointF(265, Y_NFC));
+ primitiveComposer.SetLineDash(new org.pdfclown.documents.contents.LineDash(new double[] { 3, 2 }));
+ primitiveComposer.Stroke();
+ primitiveComposer.End();
+ }
+ }
+}
diff --git a/DanfeSharp/Blocos/SimplificadoTipo2/BlocoIdentificacaoT2.cs b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoIdentificacaoT2.cs
new file mode 100644
index 0000000..5601396
--- /dev/null
+++ b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoIdentificacaoT2.cs
@@ -0,0 +1,42 @@
+using DanfeSharp.Modelo;
+using org.pdfclown.documents.contents.composition;
+using System.Drawing;
+
+namespace DanfeSharp.Blocos.SimplificadoTipo2
+{
+ ///
+ /// Divisão VII (NT 2026.003) — identificação da NF-e: número, série, data/hora de
+ /// emissão em horário local, rótulo da via e protocolo de autorização (quando
+ /// autorizada).
+ ///
+ internal class BlocoIdentificacaoT2 : ElementoBase
+ {
+ public BlocoIdentificacaoT2(DanfeViewModel viewModel, Estilo estilo, PrimitiveComposer primitiveComposer, float y, string rotuloVia) : base(estilo)
+ {
+ primitiveComposer.BeginLocalState();
+
+ primitiveComposer.SetFont(estilo.FonteCampoTituloNegrito.FonteInterna, estilo.FonteCampoTituloNegrito.Tamanho);
+ primitiveComposer.ShowText($"NF-e Nº: {viewModel.NfNumero} Série: {viewModel.NfSerie}", new PointF(140, y + 10), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+
+ primitiveComposer.SetFont(estilo.FonteCampoConteudoNegrito.FonteInterna, estilo.FonteCampoConteudoNegrito.Tamanho);
+ primitiveComposer.ShowText($"{viewModel.DataHoraEmissao.FormatarDataHoraWithoutGMT()} - {rotuloVia}", new PointF(140, y + 20), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+
+ Y_NFC = y + 20;
+
+ if (!string.IsNullOrWhiteSpace(viewModel.ProtocoloAutorizacao))
+ {
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, estilo.FonteCampoConteudo.Tamanho);
+ primitiveComposer.ShowText("PROTOCOLO DE AUTORIZAÇÃO", new PointF(140, Y_NFC + 10), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(viewModel.ProtocoloAutorizacao, new PointF(140, Y_NFC + 20), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ Y_NFC += 20;
+ }
+
+ Y_NFC += 10;
+
+ primitiveComposer.DrawLine(new PointF(15, Y_NFC), new PointF(265, Y_NFC));
+ primitiveComposer.SetLineDash(new org.pdfclown.documents.contents.LineDash(new double[] { 3, 2 }));
+ primitiveComposer.Stroke();
+ primitiveComposer.End();
+ }
+ }
+}
diff --git a/DanfeSharp/Blocos/SimplificadoTipo2/BlocoMensagemContingenciaT2.cs b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoMensagemContingenciaT2.cs
new file mode 100644
index 0000000..640c368
--- /dev/null
+++ b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoMensagemContingenciaT2.cs
@@ -0,0 +1,36 @@
+using DanfeSharp.Modelo;
+using org.pdfclown.documents.contents.composition;
+using System.Drawing;
+
+namespace DanfeSharp.Blocos.SimplificadoTipo2
+{
+ ///
+ /// Divisão VIII (NT 2026.003) — mensagem de contingência "EMITIDA EM CONTINGÊNCIA /
+ /// PENDENTE DE AUTORIZAÇÃO", centralizada, caixa alta, 2 linhas. Instanciado em dois
+ /// locais: abaixo do cabeçalho e abaixo da identificação da NF-e. Não renderiza nada
+ /// quando a nota não está pendente de autorização.
+ ///
+ internal class BlocoMensagemContingenciaT2 : ElementoBase
+ {
+ public BlocoMensagemContingenciaT2(DanfeViewModel viewModel, Estilo estilo, PrimitiveComposer primitiveComposer, float y) : base(estilo)
+ {
+ if (!viewModel.PendenteAutorizacao)
+ {
+ Y_NFC = y;
+ return;
+ }
+
+ primitiveComposer.BeginLocalState();
+ primitiveComposer.SetFont(estilo.FonteCampoConteudoNegrito.FonteInterna, estilo.FonteCampoConteudoNegrito.Tamanho);
+ primitiveComposer.ShowText("EMITIDA EM CONTINGÊNCIA", new PointF(140, y + 10), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText("PENDENTE DE AUTORIZAÇÃO", new PointF(140, y + 20), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+
+ Y_NFC = y + 30;
+
+ primitiveComposer.DrawLine(new PointF(15, Y_NFC), new PointF(265, Y_NFC));
+ primitiveComposer.SetLineDash(new org.pdfclown.documents.contents.LineDash(new double[] { 3, 2 }));
+ primitiveComposer.Stroke();
+ primitiveComposer.End();
+ }
+ }
+}
diff --git a/DanfeSharp/Blocos/SimplificadoTipo2/BlocoMensagensT2.cs b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoMensagensT2.cs
new file mode 100644
index 0000000..ceeb171
--- /dev/null
+++ b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoMensagensT2.cs
@@ -0,0 +1,70 @@
+using DanfeSharp.Modelo;
+using org.pdfclown.documents.contents.composition;
+using System;
+using System.Drawing;
+
+namespace DanfeSharp.Blocos.SimplificadoTipo2
+{
+ ///
+ /// Divisões VIII/IX (NT 2026.003) — mensagem fiscal (infAdFisco) e mensagem do
+ /// contribuinte (infCpl + tributos aproximados da Lei 12.741/2012).
+ ///
+ internal class BlocoMensagensT2 : ElementoBase
+ {
+ private const int MaxLength = 80;
+
+ public BlocoMensagensT2(DanfeViewModel viewModel, Estilo estilo, PrimitiveComposer primitiveComposer, float y) : base(estilo)
+ {
+ Y_NFC = y;
+
+ primitiveComposer.BeginLocalState();
+
+ if (!string.IsNullOrWhiteSpace(viewModel.InformacoesAdicionaisFisco))
+ {
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, 7);
+ MostrarTextoQuebrado(primitiveComposer, viewModel.InformacoesAdicionaisFisco);
+ }
+
+ if (viewModel.CalculoImposto.ValorAproximadoTributos > 0)
+ {
+ primitiveComposer.SetFont(estilo.FonteCampoConteudoNegrito.FonteInterna, 7);
+
+ Y_NFC += 10;
+ primitiveComposer.ShowText($"CONFORME LEI 12.741/2012 o valor aproximado dos tributos é {viewModel.CalculoImposto.ValorAproximadoTributos.Formatar()}", new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+
+ Y_NFC += 10;
+ primitiveComposer.ShowText($"O valor aproximado dos tributos Federais é {viewModel.CalculoImposto.ValorAproximadoTributosFederais.Formatar()}", new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+
+ Y_NFC += 10;
+ primitiveComposer.ShowText($"O valor aproximado dos tributos Estaduais é {viewModel.CalculoImposto.ValorAproximadoTributosEstaduais.Formatar()}", new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ }
+
+ if (!string.IsNullOrWhiteSpace(viewModel.InformacoesComplementares))
+ {
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, 7);
+ MostrarTextoQuebrado(primitiveComposer, viewModel.InformacoesComplementares);
+ }
+
+ primitiveComposer.End();
+ }
+
+ // Quebra o texto em linhas de até MaxLength caracteres (máx. 2, como no cupom
+ // NFC-e), centralizadas.
+ private void MostrarTextoQuebrado(PrimitiveComposer primitiveComposer, string texto)
+ {
+ Y_NFC += 10;
+
+ if (texto.Length > MaxLength)
+ {
+ primitiveComposer.ShowText(texto.Substring(0, MaxLength), new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+
+ Y_NFC += 10;
+ primitiveComposer.ShowText(texto.Substring(MaxLength, Math.Min(texto.Length - MaxLength, MaxLength)), new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ }
+ else
+ {
+ primitiveComposer.ShowText(texto, new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Middle, 0);
+ }
+ }
+ }
+}
diff --git a/DanfeSharp/Blocos/SimplificadoTipo2/BlocoQrCodeT2.cs b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoQrCodeT2.cs
new file mode 100644
index 0000000..cc26676
--- /dev/null
+++ b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoQrCodeT2.cs
@@ -0,0 +1,56 @@
+using DanfeSharp.Modelo;
+using org.pdfclown.documents;
+using org.pdfclown.documents.contents.composition;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+
+namespace DanfeSharp.Blocos.SimplificadoTipo2
+{
+ ///
+ /// Divisão V (NT 2026.003) — QR Code de consulta, mínimo 25×25 mm (≈ 70,9 pt) com
+ /// quiet zone de 3 mm. Renderizado apenas quando o consumer populou
+ /// ; a geração do conteúdo (QR v3 para mod. 55)
+ /// é responsabilidade de quem monta o ViewModel.
+ ///
+ internal class BlocoQrCodeT2 : ElementoBase
+ {
+ // 120 pt ≈ 42 mm, acima do mínimo de 25 mm da NT; quiet zone garantida pela
+ // centralização na largura de 280 pt.
+ private const float TamanhoQrCode = 120;
+
+ public BlocoQrCodeT2(DanfeViewModel viewModel, Estilo estilo, PrimitiveComposer primitiveComposer, float y, Document context) : base(estilo)
+ {
+ if (string.IsNullOrWhiteSpace(viewModel.QrCode))
+ {
+ Y_NFC = y;
+ return;
+ }
+
+ primitiveComposer.BeginLocalState();
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, estilo.FonteCampoConteudo.Tamanho);
+
+ Y_NFC = y + 10;
+ primitiveComposer.ShowText("CONSULTA VIA LEITOR DE QR CODE", new PointF(140, Y_NFC), XAlignmentEnum.Center, YAlignmentEnum.Top, 0);
+ Y_NFC += 10;
+
+ using (var result = new MemoryStream())
+ {
+ var bitmap = GerarQrCode.GerarQRCode(viewModel.QrCode);
+ bitmap.Save(result, ImageFormat.Jpeg);
+ result.Position = 0;
+
+ var image = org.pdfclown.documents.contents.entities.Image.Get(result);
+ var imageXObject = image.ToXObject(context);
+ primitiveComposer.ShowXObject(imageXObject, new PointF(140, Y_NFC), new SizeF(TamanhoQrCode, TamanhoQrCode), XAlignmentEnum.Center, YAlignmentEnum.Top, 0);
+ }
+
+ Y_NFC += TamanhoQrCode + 10;
+
+ primitiveComposer.DrawLine(new PointF(15, Y_NFC), new PointF(265, Y_NFC));
+ primitiveComposer.SetLineDash(new org.pdfclown.documents.contents.LineDash(new double[] { 3, 2 }));
+ primitiveComposer.Stroke();
+ primitiveComposer.End();
+ }
+ }
+}
diff --git a/DanfeSharp/Blocos/SimplificadoTipo2/BlocoTotaisSimplificadoT2.cs b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoTotaisSimplificadoT2.cs
new file mode 100644
index 0000000..cc0989b
--- /dev/null
+++ b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoTotaisSimplificadoT2.cs
@@ -0,0 +1,79 @@
+using DanfeSharp.Modelo;
+using org.pdfclown.documents.contents.composition;
+using System.Drawing;
+
+namespace DanfeSharp.Blocos.SimplificadoTipo2
+{
+ ///
+ /// Divisão III (NT 2026.003) — totais da operação: quantidade de itens, valor dos
+ /// produtos, acréscimos/descontos, valor total, formas de pagamento, valor pago e
+ /// troco (a linha TROCO é sempre impressa, ainda que 0,00).
+ ///
+ internal class BlocoTotaisSimplificadoT2 : ElementoBase
+ {
+ public BlocoTotaisSimplificadoT2(DanfeViewModel viewModel, Estilo estilo, PrimitiveComposer primitiveComposer, float y) : base(estilo)
+ {
+ primitiveComposer.BeginLocalState();
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, estilo.FonteCampoConteudo.Tamanho);
+
+ Y_NFC = y + 10;
+ primitiveComposer.ShowText("QTD. TOTAL DE ITENS", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(viewModel.CalculoImposto.QuantidadeTotal.ToString(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+
+ Y_NFC += 10;
+ primitiveComposer.ShowText("VALOR DOS PRODUTOS", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(viewModel.CalculoImposto.ValorTotalProdutos.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+
+ var acrescimos = viewModel.CalculoImposto.ValorFrete + viewModel.CalculoImposto.ValorSeguro + viewModel.CalculoImposto.OutrasDespesas;
+
+ if (acrescimos > 0)
+ {
+ Y_NFC += 10;
+ primitiveComposer.ShowText("ACRÉSCIMOS (+)", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(acrescimos.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ }
+
+ if (viewModel.CalculoImposto.Desconto > 0)
+ {
+ Y_NFC += 10;
+ primitiveComposer.ShowText("DESCONTO (-)", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(viewModel.CalculoImposto.Desconto.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ }
+
+ Y_NFC += 10;
+ primitiveComposer.SetFont(estilo.FonteCampoTituloNegrito.FonteInterna, estilo.FonteCampoTituloNegrito.Tamanho);
+ primitiveComposer.ShowText("VALOR TOTAL", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(viewModel.CalculoImposto.ValorTotalNota.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+
+ Y_NFC += 10;
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, estilo.FonteCampoConteudo.Tamanho);
+ primitiveComposer.ShowText("FORMAS DE PAGAMENTO", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+
+ foreach (var pagamento in viewModel.Pagamento)
+ {
+ if (pagamento.DetalhePagamento == null) continue;
+
+ foreach (var detalhe in pagamento.DetalhePagamento)
+ {
+ Y_NFC += 10;
+ primitiveComposer.ShowText(detalhe.FormaPagamento.FormaPagamentoToString(), new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(detalhe.Valor.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ }
+ }
+
+ Y_NFC += 10;
+ primitiveComposer.ShowText("VALOR PAGO", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(viewModel.ValorPagoTotal.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+
+ Y_NFC += 10;
+ primitiveComposer.ShowText("TROCO", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(viewModel.TrocoTotal.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+
+ Y_NFC += 10;
+ primitiveComposer.DrawLine(new PointF(15, Y_NFC), new PointF(265, Y_NFC));
+ primitiveComposer.SetLineDash(new org.pdfclown.documents.contents.LineDash(new double[] { 3, 2 }));
+ primitiveComposer.Stroke();
+ primitiveComposer.End();
+ }
+ }
+}
diff --git a/DanfeSharp/Blocos/SimplificadoTipo2/BlocoTributosIbsCbsT2.cs b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoTributosIbsCbsT2.cs
new file mode 100644
index 0000000..dd6449e
--- /dev/null
+++ b/DanfeSharp/Blocos/SimplificadoTipo2/BlocoTributosIbsCbsT2.cs
@@ -0,0 +1,81 @@
+using DanfeSharp.Modelo;
+using org.pdfclown.documents.contents.composition;
+using System.Drawing;
+
+namespace DanfeSharp.Blocos.SimplificadoTipo2
+{
+ ///
+ /// Divisão III-A (NT 2026.003) — discriminação dos tributos da Reforma Tributária
+ /// (IBS, CBS e IS quando houver). Omitido por inteiro quando o XML não tem o grupo
+ /// IBSCBSTot (fidelidade ao XML).
+ ///
+ internal class BlocoTributosIbsCbsT2 : ElementoBase
+ {
+ public BlocoTributosIbsCbsT2(DanfeViewModel viewModel, Estilo estilo, PrimitiveComposer primitiveComposer, float y) : base(estilo)
+ {
+ var tributos = viewModel.TributosIbsCbs;
+
+ if (tributos == null)
+ {
+ Y_NFC = y;
+ return;
+ }
+
+ primitiveComposer.BeginLocalState();
+
+ Y_NFC = y + 10;
+ primitiveComposer.SetFont(estilo.FonteCampoConteudoNegrito.FonteInterna, estilo.FonteCampoConteudoNegrito.Tamanho);
+ primitiveComposer.ShowText("TRIBUTOS IBS/CBS", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, estilo.FonteCampoConteudo.Tamanho);
+
+ if (tributos.BaseCalculo.HasValue)
+ {
+ Y_NFC += 10;
+ primitiveComposer.ShowText("BASE DE CÁLCULO IBS/CBS", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(tributos.BaseCalculo.Value.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ }
+
+ if (tributos.ValorIbs.HasValue)
+ {
+ Y_NFC += 10;
+ primitiveComposer.ShowText("IBS", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(tributos.ValorIbs.Value.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ }
+
+ if (tributos.ValorIbsUF.HasValue)
+ {
+ Y_NFC += 10;
+ primitiveComposer.ShowText(" IBS ESTADUAL (UF)", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(tributos.ValorIbsUF.Value.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ }
+
+ if (tributos.ValorIbsMun.HasValue)
+ {
+ Y_NFC += 10;
+ primitiveComposer.ShowText(" IBS MUNICIPAL", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(tributos.ValorIbsMun.Value.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ }
+
+ if (tributos.ValorCbs.HasValue)
+ {
+ Y_NFC += 10;
+ primitiveComposer.ShowText("CBS", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(tributos.ValorCbs.Value.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ }
+
+ if (tributos.ValorIS.HasValue)
+ {
+ Y_NFC += 10;
+ primitiveComposer.ShowText("IMPOSTO SELETIVO (IS)", new PointF(25, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(tributos.ValorIS.Value.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ }
+
+ Y_NFC += 10;
+ primitiveComposer.DrawLine(new PointF(15, Y_NFC), new PointF(265, Y_NFC));
+ primitiveComposer.SetLineDash(new org.pdfclown.documents.contents.LineDash(new double[] { 3, 2 }));
+ primitiveComposer.Stroke();
+ primitiveComposer.End();
+ }
+ }
+}
diff --git a/DanfeSharp/Blocos/SimplificadoTipo2/TabelaProdutosSimplificadoT2.cs b/DanfeSharp/Blocos/SimplificadoTipo2/TabelaProdutosSimplificadoT2.cs
new file mode 100644
index 0000000..4b1373b
--- /dev/null
+++ b/DanfeSharp/Blocos/SimplificadoTipo2/TabelaProdutosSimplificadoT2.cs
@@ -0,0 +1,63 @@
+using DanfeSharp.Modelo;
+using org.pdfclown.documents.contents.composition;
+using System.Drawing;
+
+namespace DanfeSharp.Blocos.SimplificadoTipo2
+{
+ ///
+ /// Divisão II (NT 2026.003) — produtos/serviços: código, descrição, quantidade,
+ /// unidade, valor unitário e valor total por item.
+ ///
+ internal class TabelaProdutosSimplificadoT2 : ElementoBase
+ {
+ public TabelaProdutosSimplificadoT2(DanfeViewModel viewModel, Estilo estilo, PrimitiveComposer primitiveComposer, float y) : base(estilo)
+ {
+ primitiveComposer.BeginLocalState();
+
+ Y_NFC = y + 10;
+
+ primitiveComposer.SetFont(estilo.FonteCampoConteudo.FonteInterna, estilo.FonteCampoConteudo.Tamanho);
+ primitiveComposer.ShowText("CÓDIGO", new PointF(65, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText("DESCRIÇÃO", new PointF(70, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+
+ Y_NFC += 10;
+
+ primitiveComposer.ShowText("QTD.", new PointF(70, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText("UNID.", new PointF(100, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText("PREÇO", new PointF(200, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText("TOTAL", new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+
+ Y_NFC += 10;
+
+ // Stroke imediato: texto emitido com path aberto é inválido pela spec do
+ // PDF e some na extração/renderização estrita.
+ primitiveComposer.DrawLine(new PointF(15, Y_NFC), new PointF(265, Y_NFC));
+ primitiveComposer.SetLineDash(new org.pdfclown.documents.contents.LineDash(new double[] { 3, 2 }));
+ primitiveComposer.Stroke();
+
+ foreach (var produto in viewModel.Produtos)
+ {
+ Y_NFC += 10;
+
+ primitiveComposer.ShowText(produto.Codigo.Length > 15 ? produto.Codigo.Substring(0, 10) : produto.Codigo,
+ new PointF(65, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(produto.Descricao.Length > 40 ? produto.Descricao.Substring(0, 40) : produto.Descricao,
+ new PointF(70, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+
+ Y_NFC += 10;
+
+ primitiveComposer.ShowText(produto.Quantidade.Formatar(), new PointF(90, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(produto.Unidade, new PointF(100, Y_NFC), XAlignmentEnum.Left, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(produto.ValorUnitario.Formatar(), new PointF(200, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ primitiveComposer.ShowText(produto.ValorTotal.Formatar(), new PointF(250, Y_NFC), XAlignmentEnum.Right, YAlignmentEnum.Middle, 0);
+ }
+
+ Y_NFC += 10;
+
+ primitiveComposer.DrawLine(new PointF(15, Y_NFC), new PointF(265, Y_NFC));
+ primitiveComposer.SetLineDash(new org.pdfclown.documents.contents.LineDash(new double[] { 3, 2 }));
+ primitiveComposer.Stroke();
+ primitiveComposer.End();
+ }
+ }
+}
diff --git a/DanfeSharp/DanfeSimplificadoTipo2.cs b/DanfeSharp/DanfeSimplificadoTipo2.cs
new file mode 100644
index 0000000..324ecaa
--- /dev/null
+++ b/DanfeSharp/DanfeSimplificadoTipo2.cs
@@ -0,0 +1,221 @@
+using DanfeSharp.Blocos.NFC;
+using DanfeSharp.Blocos.SimplificadoTipo2;
+using DanfeSharp.Modelo;
+using org.pdfclown.documents;
+using org.pdfclown.documents.contents.composition;
+using org.pdfclown.documents.contents.fonts;
+using org.pdfclown.files;
+using System;
+using System.Drawing;
+
+namespace DanfeSharp
+{
+ ///
+ /// DANFE Simplificado Tipo 2 (NT 2026.003) — documento auxiliar em formato
+ /// cupom para NF-e modelo 55 emitida com tpImp=6, nas 9 divisões da NT.
+ /// Mesmo padrão do : página estreita (280 pt ≈ 98,8 mm,
+ /// acima do mínimo de 56 mm), altura dinâmica e blocos empilhados. Em contingência
+ /// pendente de autorização gera duas vias ("Via do Consumidor" e "Via do
+ /// Estabelecimento"), conforme a Divisão VIII.
+ ///
+ public class DanfeSimplificadoTipo2 : IDisposable
+ {
+ private const float Width = 280;
+
+ private readonly StandardType1Font _FonteRegular;
+ private readonly StandardType1Font _FonteNegrito;
+ private readonly StandardType1Font _FonteItalico;
+ private readonly StandardType1Font.FamilyEnum _FonteFamilia;
+ private Boolean _FoiGerado;
+ private readonly string _creditos;
+ private readonly string _metadataCriador;
+ private readonly SizeF _size;
+
+ public DanfeViewModel ViewModel { get; private set; }
+ public File File { get; private set; }
+ internal Document PdfDocument { get; private set; }
+ internal Estilo EstiloPadrao { get; private set; }
+
+ public DanfeSimplificadoTipo2(DanfeViewModel viewModel, string creditos = null, string metadataCriador = null)
+ {
+ ViewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
+
+ // Divisão IV — sem URL informada pelo consumer, usa a consulta do portal
+ // nacional da NF-e, válida para qualquer UF.
+ if (string.IsNullOrWhiteSpace(viewModel.EndConsulta))
+ {
+ viewModel.EndConsulta = (viewModel.Emitente?.EnderecoUf).UrlNFeConsulta(viewModel.TipoAmbiente);
+ }
+
+ File = new File();
+ PdfDocument = File.Document;
+
+ _size = new SizeF(Width, EstimarAltura(viewModel));
+
+ _creditos = creditos ?? "Impresso com DanfeSharp";
+ _metadataCriador = metadataCriador ?? String.Format("{0} {1} - {2}", "DanfeSharp", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version, "https://github.com/SilverCard/DanfeSharp");
+
+ _FonteFamilia = StandardType1Font.FamilyEnum.Helvetica;
+ _FonteRegular = new StandardType1Font(PdfDocument, _FonteFamilia, false, false);
+ _FonteNegrito = new StandardType1Font(PdfDocument, _FonteFamilia, true, false);
+ _FonteItalico = new StandardType1Font(PdfDocument, _FonteFamilia, false, true);
+
+ EstiloPadrao = new Estilo(_FonteRegular, _FonteNegrito, _FonteItalico, 9, 8);
+
+ AdicionarMetadata();
+
+ _FoiGerado = false;
+ }
+
+ // Estima a altura da página acompanhando os incrementos reais de cada bloco,
+ // com folga — o PDFClown 0.2.0-beta não permite redimensionar a página depois
+ // da composição.
+ private static float EstimarAltura(DanfeViewModel viewModel)
+ {
+ float altura = 100; // Divisão I — cabeçalho + folga
+
+ if (viewModel.PendenteAutorizacao)
+ altura += 60; // Divisão VIII — contingência, 2 locais
+
+ if (viewModel.TipoAmbiente == 2)
+ altura += 20; // Divisão VIII — homologação
+
+ altura += 40 + viewModel.Produtos.Count * 20; // Divisão II — cabeçalho + 2 linhas/item
+
+ altura += 90; // Divisão III — totais fixos + valor pago + troco
+ foreach (var pagamento in viewModel.Pagamento)
+ altura += (pagamento.DetalhePagamento?.Count ?? 0) * 10;
+
+ if (viewModel.TributosIbsCbs != null)
+ altura += 90; // Divisão III-A
+
+ altura += 60; // Divisão IV — consulta chave
+
+ if (!string.IsNullOrWhiteSpace(viewModel.QrCode))
+ altura += 160; // Divisão V — QR Code
+
+ altura += 60; // Divisão VI — consumidor
+
+ altura += 60; // Divisão VII — identificação
+
+ if (!string.IsNullOrWhiteSpace(viewModel.InformacoesAdicionaisFisco))
+ altura += 30; // Divisão VIII — infAdFisco
+
+ if (viewModel.CalculoImposto.ValorAproximadoTributos > 0)
+ altura += 40; // Divisão IX — Lei 12.741
+
+ if (!string.IsNullOrWhiteSpace(viewModel.InformacoesComplementares))
+ altura += 30; // Divisão IX — infCpl
+
+ return altura + 30; // folga final
+ }
+
+ private void AdicionarMetadata()
+ {
+ var info = PdfDocument.Information;
+ info[new org.pdfclown.objects.PdfName("ChaveAcesso")] = ViewModel.ChaveAcesso;
+ info[new org.pdfclown.objects.PdfName("TipoDocumento")] = "DANFE";
+ info.CreationDate = DateTime.Now;
+ info.Title = "DANFE Simplificado Tipo 2 (Documento auxiliar da NFe)";
+ info.Creator = _metadataCriador;
+ }
+
+ public void Gerar()
+ {
+ if (_FoiGerado) throw new InvalidOperationException("O Danfe já foi gerado.");
+
+ ComporVia("Via do Consumidor");
+
+ // Divisão VIII — em contingência pendente de autorização a NT exige a
+ // impressão de segunda via identificada, guardada até a autorização.
+ if (ViewModel.PendenteAutorizacao)
+ {
+ ComporVia("Via do Estabelecimento");
+ }
+
+ _FoiGerado = true;
+ }
+
+ private void ComporVia(string rotuloVia)
+ {
+ var page = new Page(PdfDocument, _size);
+ PdfDocument.Pages.Add(page);
+
+ var composer = new PrimitiveComposer(page);
+
+ // Divisão I
+ var cabecalho = new BlocoCabecalhoSimplificadoT2(ViewModel, EstiloPadrao, composer);
+
+ // Divisão VIII — contingência (1ª ocorrência, abaixo do cabeçalho)
+ var contingenciaTopo = new BlocoMensagemContingenciaT2(ViewModel, EstiloPadrao, composer, cabecalho.Y_NFC);
+
+ // Divisão VIII — homologação (bloco NFC reusado: condiciona por TipoAmbiente == 2)
+ var informacaoFiscal = new BlocoInformacaoFiscal(ViewModel, EstiloPadrao, composer, contingenciaTopo.Y_NFC);
+
+ // Divisão II
+ var tabelaProdutos = new TabelaProdutosSimplificadoT2(ViewModel, EstiloPadrao, composer, informacaoFiscal.Y_NFC);
+
+ // Divisão III
+ var totais = new BlocoTotaisSimplificadoT2(ViewModel, EstiloPadrao, composer, tabelaProdutos.Y_NFC);
+
+ // Divisão III-A
+ var tributosIbsCbs = new BlocoTributosIbsCbsT2(ViewModel, EstiloPadrao, composer, totais.Y_NFC);
+
+ // Divisão IV
+ var consultaChave = new BlocoConsultaChaveT2(ViewModel, EstiloPadrao, composer, tributosIbsCbs.Y_NFC);
+
+ // Divisão V
+ var qrCode = new BlocoQrCodeT2(ViewModel, EstiloPadrao, composer, consultaChave.Y_NFC, PdfDocument);
+
+ // Divisão VI
+ var consumidor = new BlocoConsumidorT2(ViewModel, EstiloPadrao, composer, qrCode.Y_NFC);
+
+ // Divisão VII
+ var identificacao = new BlocoIdentificacaoT2(ViewModel, EstiloPadrao, composer, consumidor.Y_NFC, rotuloVia);
+
+ // Divisão VIII — contingência (2ª ocorrência, abaixo da identificação)
+ var contingenciaBase = new BlocoMensagemContingenciaT2(ViewModel, EstiloPadrao, composer, identificacao.Y_NFC);
+
+ // Divisões VIII/IX — mensagens fiscal e do contribuinte
+ var mensagens = new BlocoMensagensT2(ViewModel, EstiloPadrao, composer, contingenciaBase.Y_NFC);
+
+ composer.Flush();
+ }
+
+ public void Salvar(String path)
+ {
+ if (String.IsNullOrWhiteSpace(path)) throw new ArgumentException(nameof(path));
+
+ File.Save(path, SerializationModeEnum.Incremental);
+ }
+
+ public void Salvar(System.IO.Stream stream)
+ {
+ if (stream == null) throw new ArgumentNullException(nameof(stream));
+
+ File.Save(new org.pdfclown.bytes.Stream(stream), SerializationModeEnum.Incremental);
+ }
+
+ #region IDisposable Support
+ private bool disposedValue = false;
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ File.Dispose();
+ }
+
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+ #endregion
+ }
+}
diff --git a/DanfeSharp/Extentions.cs b/DanfeSharp/Extentions.cs
index 739c987..fd61242 100644
--- a/DanfeSharp/Extentions.cs
+++ b/DanfeSharp/Extentions.cs
@@ -206,6 +206,19 @@ public static string UrlNFCeProduction(this string state)
return value;
}
+ ///
+ /// URL de consulta pela chave de acesso da NF-e mod. 55 (DANFE Simplificado
+ /// Tipo 2, NT 2026.003 Divisão IV). O portal nacional atende a consulta de
+ /// qualquer UF; substituir por tabela por UF quando a SEFAZ publicar material
+ /// oficial com URLs próprias.
+ ///
+ public static string UrlNFeConsulta(this string state, int tipoAmbiente)
+ {
+ return tipoAmbiente == 2
+ ? "www.homologacao.nfe.fazenda.gov.br/portal/consultaRecaptcha.aspx"
+ : "www.nfe.fazenda.gov.br/portal/consultaRecaptcha.aspx";
+ }
+
public static string UrlNFCeTest(this string state)
{
var stateDictionary = new Dictionary
diff --git a/DanfeSharp/Modelo/DanfeViewModel.cs b/DanfeSharp/Modelo/DanfeViewModel.cs
index 41c7ac0..69692cd 100644
--- a/DanfeSharp/Modelo/DanfeViewModel.cs
+++ b/DanfeSharp/Modelo/DanfeViewModel.cs
@@ -313,6 +313,63 @@ public float Margem
/// Justificativa da entrada em contingência - xJust
///
public string ContingenciaJustificativa { get; set; }
+
+ ///
+ /// Emitida em contingência e ainda sem protocolo de autorização — gatilho
+ /// das mensagens "EMITIDA EM CONTINGÊNCIA / Pendente de autorização" e da
+ /// segunda via no DANFE Simplificado Tipo 2 (NT 2026.003, Divisão VIII).
+ ///
+ public bool PendenteAutorizacao => TipoEmissao != FormaEmissao.Normal && String.IsNullOrWhiteSpace(ProtocoloAutorizacao);
+ #endregion
+
+ #region DANFE Simplificado Tipo 2 (NT 2026.003)
+
+ ///
+ /// Totais IBS/CBS/IS (grupo IBSCBSTot) — Divisão III-A do DANFE Simplificado
+ /// Tipo 2. Nulo quando o XML não tem o grupo (divisão omitida).
+ ///
+ public TributosIbsCbsViewModel TributosIbsCbs { get; set; }
+
+ ///
+ /// Operação não presencial (indPres diferente de 1 e 5) — na NT 2026.003 a
+ /// identificação do consumidor (Divisão VI) é obrigatória nesses casos.
+ /// A validação acontece na emissão; aqui apenas informa o renderer.
+ ///
+ public bool OperacaoNaoPresencial { get; set; }
+
+ ///
+ /// Soma dos valores pagos (vPag) de todas as formas de pagamento.
+ ///
+ public decimal ValorPagoTotal
+ {
+ get
+ {
+ decimal total = 0;
+ foreach (var pagamento in Pagamento)
+ {
+ if (pagamento.DetalhePagamento == null) continue;
+ foreach (var detalhe in pagamento.DetalhePagamento)
+ total += detalhe.Valor;
+ }
+ return total;
+ }
+ }
+
+ ///
+ /// Soma do troco (vTroco) — a Divisão III imprime a linha TROCO sempre,
+ /// ainda que 0,00.
+ ///
+ public decimal TrocoTotal
+ {
+ get
+ {
+ decimal total = 0;
+ foreach (var pagamento in Pagamento)
+ total += pagamento.Troco ?? 0;
+ return total;
+ }
+ }
+
#endregion
public DanfeViewModel()
diff --git a/DanfeSharp/Modelo/TributosIbsCbsViewModel.cs b/DanfeSharp/Modelo/TributosIbsCbsViewModel.cs
new file mode 100644
index 0000000..1c04dd0
--- /dev/null
+++ b/DanfeSharp/Modelo/TributosIbsCbsViewModel.cs
@@ -0,0 +1,41 @@
+namespace DanfeSharp.Modelo
+{
+ ///
+ /// Totais dos tributos da Reforma Tributária (grupo IBSCBSTot) para a
+ /// Divisão III-A do DANFE Simplificado Tipo 2 (NT 2026.003).
+ /// Instância nula no significa
+ /// que o XML não tem o grupo — a divisão é omitida (fidelidade ao XML).
+ ///
+ public class TributosIbsCbsViewModel
+ {
+ ///
+ /// Base de cálculo comum do IBS e da CBS (vBCIBSCBS)
+ ///
+ public decimal? BaseCalculo { get; set; }
+
+ ///
+ /// Valor total do IBS (vIBS)
+ ///
+ public decimal? ValorIbs { get; set; }
+
+ ///
+ /// Parcela estadual do IBS (vIBSUF)
+ ///
+ public decimal? ValorIbsUF { get; set; }
+
+ ///
+ /// Parcela municipal do IBS (vIBSMun)
+ ///
+ public decimal? ValorIbsMun { get; set; }
+
+ ///
+ /// Valor total da CBS (vCBS)
+ ///
+ public decimal? ValorCbs { get; set; }
+
+ ///
+ /// Valor total do Imposto Seletivo (vIS), quando houver
+ ///
+ public decimal? ValorIS { get; set; }
+ }
+}
diff --git a/openspec/specs/danfe-simplificado-tipo-2/spec.md b/openspec/specs/danfe-simplificado-tipo-2/spec.md
new file mode 100644
index 0000000..d94fd61
--- /dev/null
+++ b/openspec/specs/danfe-simplificado-tipo-2/spec.md
@@ -0,0 +1,127 @@
+# danfe-simplificado-tipo-2 Specification
+
+## Purpose
+
+Define como o `nfe/DanfeSharp` renderiza o **DANFE Simplificado Tipo 2** — documento auxiliar em formato cupom para NF-e **modelo 55** emitida com `tpImp=6`, voltado ao varejo (operações que hoje seriam NFC-e). Renderização via classe dedicada `DanfeSimplificadoTipo2` (mesmo padrão de `DanfeNFC`: página estreita de altura dinâmica, blocos empilhados), consumindo o `DanfeViewModel` compartilhado.
+
+**Base normativa:** NT 2026.003 v1.00 (especificações técnicas do DANFE Simplificado Tipo 2), em atendimento à NT 2026.002 v1.00 e aos Ajustes SINIEF 32/2025 e 13/2026. Produção: 03/08/2026. A NT define **9 divisões** com conteúdo mínimo, papel de largura ≥ 56 mm, impressora comum e fidelidade ao XML (proibido imprimir dado que não conste no XML, exceto protocolo, cMsg/xMsg).
+
+## Requirements
+
+### Requirement: Divisão I — Cabeçalho
+
+O cupom SHALL iniciar com a identificação do emitente (razão social, CNPJ, endereço) e o título destacado **"DANFE Simplificado Tipo 2"**. Logotipo é opcional.
+
+#### Scenario: Cabeçalho com emitente e título
+
+- **GIVEN** um `DanfeViewModel` com emitente preenchido
+- **WHEN** o PDF é gerado
+- **THEN** o texto extraído contém a razão social, o CNPJ formatado e "DANFE Simplificado Tipo 2"
+
+### Requirement: Divisão II — Produtos e Divisão III — Totais
+
+A tabela de produtos SHALL exibir código, descrição, quantidade, unidade, valor unitário e valor total por item. Os totais SHALL exibir quantidade total de itens, valor dos produtos, desconto/acréscimos, valor total, as formas de pagamento com valores, a linha "VALOR PAGO" e a linha "TROCO" — o troco é **sempre impresso**, ainda que 0,00.
+
+#### Scenario: Troco sempre presente
+
+- **GIVEN** um `DanfeViewModel` sem `Pagamento[].Troco` informado
+- **WHEN** o PDF é gerado
+- **THEN** o texto extraído contém "TROCO"
+
+### Requirement: Divisão III-A — IBS/CBS/IS
+
+Quando o ViewModel tiver `TributosIbsCbs` preenchido, o cupom SHALL discriminar CBS, IBS (com parcelas UF/Município quando presentes) e IS (quando houver). Quando `TributosIbsCbs == null`, a divisão SHALL ser **omitida** (fidelidade ao XML).
+
+#### Scenario: Com grupo IBS/CBS
+
+- **GIVEN** `TributosIbsCbs` com valores de IBS e CBS
+- **WHEN** o PDF é gerado
+- **THEN** o texto contém os rótulos de IBS e CBS com os valores
+
+#### Scenario: Sem grupo IBS/CBS
+
+- **GIVEN** `TributosIbsCbs == null`
+- **WHEN** o PDF é gerado
+- **THEN** o texto NÃO contém rótulos de IBS/CBS
+
+### Requirement: Divisão IV — Consulta via chave de acesso
+
+O cupom SHALL exibir a URL de consulta da SEFAZ e a chave de acesso de 44 dígitos formatada em **11 blocos de 4 dígitos** separados por espaço.
+
+#### Scenario: Chave em 11 blocos
+
+- **GIVEN** uma chave de acesso de 44 dígitos
+- **WHEN** o PDF é gerado
+- **THEN** o texto contém a chave com espaços a cada 4 dígitos
+
+### Requirement: Divisão V — QR Code
+
+Quando `DanfeViewModel.QrCode` estiver preenchido, o cupom SHALL renderizar o QR Code com no mínimo 25×25 mm e quiet zone de 3 mm. Quando ausente, a divisão SHALL ser omitida sem erro (a geração do conteúdo do QR v3 para mod. 55 é responsabilidade do consumer).
+
+#### Scenario: QR presente
+
+- **GIVEN** `QrCode` preenchido
+- **WHEN** o PDF é gerado
+- **THEN** a imagem do QR é renderizada e o rótulo de consulta via QR Code aparece
+
+#### Scenario: QR ausente
+
+- **GIVEN** `QrCode` nulo ou vazio
+- **WHEN** o PDF é gerado
+- **THEN** a geração conclui sem exceção e sem o rótulo de QR Code
+
+### Requirement: Divisão VI — Consumidor
+
+Quando o destinatário estiver identificado, o cupom SHALL exibir "CONSUMIDOR" com CNPJ/CPF, nome e endereço (quando presentes no XML). Quando não identificado, SHALL exibir "CONSUMIDOR NÃO IDENTIFICADO". A obrigatoriedade de identificação em operação não presencial é validada na emissão (domínio), não no renderer.
+
+#### Scenario: Consumidor identificado
+
+- **GIVEN** destinatário com CPF e nome
+- **WHEN** o PDF é gerado
+- **THEN** o texto contém "CONSUMIDOR" e o CPF
+
+### Requirement: Divisão VII — Identificação da NF-e
+
+O cupom SHALL exibir número, série, data/hora de emissão em **horário local** (não UTC — conversão é responsabilidade do consumer ao popular `DataHoraEmissao`), o rótulo da via e o protocolo de autorização com data/hora.
+
+#### Scenario: Identificação completa
+
+- **GIVEN** NF-e autorizada com protocolo
+- **WHEN** o PDF é gerado
+- **THEN** o texto contém número, série, "PROTOCOLO DE AUTORIZAÇÃO" e o protocolo
+
+### Requirement: Divisão VIII — Mensagem fiscal, contingência e homologação
+
+Em ambiente de homologação (`TipoAmbiente == 2`), o cupom SHALL exibir "EMITIDA EM AMBIENTE DE HOMOLOGAÇÃO - SEM VALOR FISCAL". Em contingência pendente de autorização (`TipoEmissao != Normal` e sem protocolo), o cupom SHALL exibir "EMITIDA EM CONTINGÊNCIA" / "Pendente de autorização" (caixa alta, centralizado, 2 linhas) em **dois locais** — abaixo do cabeçalho e abaixo da identificação — e SHALL gerar **segunda via** com rótulo "Via do Estabelecimento". `infAdFisco` SHALL ser impresso quando presente.
+
+#### Scenario: Contingência pendente
+
+- **GIVEN** `TipoEmissao = ContingenciaFSDA` e `ProtocoloAutorizacao` vazio
+- **WHEN** o PDF é gerado
+- **THEN** "EMITIDA EM CONTINGÊNCIA" aparece 4 vezes no total (2 locais × 2 vias) e a segunda página contém "Via do Estabelecimento"
+
+#### Scenario: Emissão normal autorizada
+
+- **GIVEN** `TipoEmissao = Normal` com protocolo
+- **WHEN** o PDF é gerado
+- **THEN** o PDF tem 1 página e não contém "EMITIDA EM CONTINGÊNCIA"
+
+### Requirement: Divisão IX — Mensagem do contribuinte
+
+O cupom SHALL imprimir `infCpl` quando presente e a demonstração dos tributos aproximados (Lei 12.741/2012) quando `CalculoImposto.ValorAproximadoTributos > 0`.
+
+#### Scenario: Tributos aproximados
+
+- **GIVEN** `ValorAproximadoTributos > 0`
+- **WHEN** o PDF é gerado
+- **THEN** o texto contém a referência à Lei 12.741
+
+### Requirement: Zero regression nos leiautes existentes
+
+A introdução do `DanfeSimplificadoTipo2` SHALL NOT alterar a renderização de `Danfe` (A4) nem de `DanfeNFC` (NFC-e). Os blocos novos vivem em `Blocos/SimplificadoTipo2/` e não modificam os blocos NFC existentes (exceção: reuso direto sem alteração de `BlocoInformacaoFiscal`).
+
+#### Scenario: Suíte existente permanece verde
+
+- **GIVEN** a suíte de testes existente do DanfeSharp
+- **WHEN** os testes rodam após a change
+- **THEN** todos os testes pré-existentes continuam passando