From dd17b373ba41a6bfa5b2ac90abcb3ed82c3a18cc Mon Sep 17 00:00:00 2001 From: rfranzoni Date: Sat, 8 Aug 2026 10:44:18 -0300 Subject: [PATCH] =?UTF-8?q?fix(infProt):=20modela=20o=20grupo=20PR13=20com?= =?UTF-8?q?o=20ocorr=C3=AAncias=20repet=C3=ADveis,=20em=20vez=20de=20par?= =?UTF-8?q?=20escalar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O leiaute do protocolo descreve o PR13 como grupo de 0 a 5 ocorrências, sem elemento agrupador ("Sequência XML"), com PR14 cMsg em 0-1 (opcional) e PR15 xMsg em 1-1 (obrigatório) — ver NT 2018.005 e a ampliação da NT 2026.002 §2.3, que criou o cStat=120 ("Autorizado o uso da NF-e, com alerta") e passou a usar o grupo para devolver até 5 alertas. infProt mapeava cMsg e xMsg em propriedades ESCALARES. Com XmlSerializer, as ocorrências repetidas colapsam: só a última sobrevive à desserialização. Uma nota autorizada com 5 alertas chegava ao consumidor com 1 — silenciosamente, e sem meio de perceber a perda. Modelagem: coleção de escolha com XmlChoiceIdentifier. É o único arranjo do XmlSerializer que preserva a ORDEM entre dois nomes de elemento distintos, e é a ordem que permite reconstruir cada ocorrência — o cMsg imediatamente anterior a um xMsg é o código daquela ocorrência. - `Mensagens` + `MensagensTipos`: a coleção crua, na ordem do documento. - `Alertas`: as ocorrências já reconstruídas em pares (Codigo, Mensagem), com Codigo anulável porque o cMsg é opcional. cMsg sem xMsg seguinte não forma ocorrência, conforme o leiaute. - `cMsg` e `xMsg` permanecem, agora como projeções da primeira ocorrência, para não quebrar quem já os consumia — NFe.Danfe.Nativo/NFCe/DanfeNativoNfce.cs usa infProt.xMsg. Também corrige o doc comment do Signature, que estava rotulado PR13 quando é PR90. Testes (NFCe.Tests/InfProtAlertGroupTests.cs): múltiplas ocorrências na ordem do documento; ocorrência sem cMsg preservada com código nulo; protocolo sem o grupo; cMsg órfão; compatibilidade dos campos escalares; e round-trip preservando a contagem exata de cMsg e xMsg. NFCe.Tests: 6 novos testes passam. A única falha da suíte (ServicosNFe_WhenNfeNFeAutorizacao4_ReturnsxMotivoSuccess) é pré-existente e sem relação — o teste aponta para um caminho absoluto de outra máquina (C:\Works\nfe\nfe-products-api\schemas). --- NFCe.Tests/InfProtAlertGroupTests.cs | 136 +++++++++++++++++++++++++++ NFe.Classes/Protocolo/infProt.cs | 123 +++++++++++++++++++++--- 2 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 NFCe.Tests/InfProtAlertGroupTests.cs diff --git a/NFCe.Tests/InfProtAlertGroupTests.cs b/NFCe.Tests/InfProtAlertGroupTests.cs new file mode 100644 index 000000000..e5fce9291 --- /dev/null +++ b/NFCe.Tests/InfProtAlertGroupTests.cs @@ -0,0 +1,136 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NFe.Classes.Protocolo; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml.Serialization; + +namespace NFCe.Tests +{ + /// + /// Grupo PR13 do protocolo (NT 2018.005, ampliado pela NT 2026.002 §2.3). + /// + /// O leiaute descreve o PR13 como grupo de 0 a 5 ocorrências, sem elemento agrupador, + /// com PR14 cMsg em 0-1 e PR15 xMsg em 1-1. Antes desta mudança + /// infProt mapeava os dois em propriedades escalares, e as ocorrências repetidas + /// colapsavam: só a última sobrevivia à desserialização. + /// + /// + [TestClass] + public class InfProtAlertGroupTests + { + private const string Namespace = "http://www.portalfiscal.inf.br/nfe"; + + private static string ProtocoloXml(string ocorrencias) + { + return + "" + + "1SVRS" + + "53260446759666000129557280000000511348419115" + + "2026-08-08T10:00:00-03:00" + + "153260000000001abc" + + "120Autorizado o uso da NF-e, com alerta" + + ocorrencias + + ""; + } + + private static protNFe Desserializar(string xml) + { + var serializer = new XmlSerializer(typeof(protNFe), Namespace); + using (var reader = new StringReader(xml)) + { + return (protNFe)serializer.Deserialize(reader); + } + } + + [TestMethod] + public void Multiplas_ocorrencias_sao_preservadas_na_ordem_do_documento() + { + var protocolo = Desserializar(ProtocoloXml( + "172destinatario bloqueado" + + "999segundo alerta" + + "111terceiro alerta")); + + var alertas = protocolo.infProt.Alertas; + + Assert.AreEqual(3, alertas.Count); + CollectionAssert.AreEqual( + new[] { 172, 999, 111 }, + alertas.Select(a => a.Codigo.Value).ToArray()); + Assert.AreEqual("destinatario bloqueado", alertas[0].Mensagem); + Assert.AreEqual("terceiro alerta", alertas[2].Mensagem); + } + + [TestMethod] + public void Ocorrencia_sem_cMsg_e_preservada_com_codigo_nulo() + { + // cMsg é 0-1 no leiaute: a SEFAZ pode retornar a mensagem sem código. + var protocolo = Desserializar(ProtocoloXml( + "172com codigo" + + "sem codigo" + + "999com codigo de novo")); + + var alertas = protocolo.infProt.Alertas; + + Assert.AreEqual(3, alertas.Count); + Assert.AreEqual(172, alertas[0].Codigo); + Assert.IsNull(alertas[1].Codigo); + Assert.AreEqual("sem codigo", alertas[1].Mensagem); + Assert.AreEqual(999, alertas[2].Codigo); + } + + [TestMethod] + public void Protocolo_sem_o_grupo_produz_lista_vazia() + { + var protocolo = Desserializar(ProtocoloXml(string.Empty)); + + Assert.AreEqual(0, protocolo.infProt.Alertas.Count); + Assert.IsNull(protocolo.infProt.cMsg); + Assert.IsNull(protocolo.infProt.xMsg); + } + + [TestMethod] + public void cMsg_sem_xMsg_seguinte_nao_forma_ocorrencia() + { + // xMsg é o campo obrigatório do grupo — sem ele não há ocorrência a reportar. + var protocolo = Desserializar(ProtocoloXml("172")); + + Assert.AreEqual(0, protocolo.infProt.Alertas.Count); + } + + [TestMethod] + public void Campos_escalares_seguem_expondo_a_primeira_ocorrencia() + { + // Compatibilidade com quem já consumia infProt.cMsg / infProt.xMsg + // (ex.: NFe.Danfe.Nativo/NFCe/DanfeNativoNfce.cs). + var protocolo = Desserializar(ProtocoloXml( + "172primeira" + + "999segunda")); + + Assert.AreEqual(172, protocolo.infProt.cMsg); + Assert.AreEqual("primeira", protocolo.infProt.xMsg); + } + + [TestMethod] + public void Round_trip_preserva_a_quantidade_de_ocorrencias() + { + var xml = ProtocoloXml( + "172com codigo" + + "sem codigo" + + "999outro"); + + var protocolo = Desserializar(xml); + + var serializer = new XmlSerializer(typeof(protNFe), Namespace); + string reserializado; + using (var writer = new StringWriter()) + { + serializer.Serialize(writer, protocolo); + reserializado = writer.ToString(); + } + + Assert.AreEqual(3, Regex.Matches(reserializado, "").Count); + Assert.AreEqual(2, Regex.Matches(reserializado, "").Count); + } + } +} diff --git a/NFe.Classes/Protocolo/infProt.cs b/NFe.Classes/Protocolo/infProt.cs index 557357890..0625a1079 100755 --- a/NFe.Classes/Protocolo/infProt.cs +++ b/NFe.Classes/Protocolo/infProt.cs @@ -31,6 +31,8 @@ /* Rua Comendador Francisco josé da Cunha, 111 - Itabaiana - SE - 49500-000 */ /********************************************************************************/ using System; +using System.Collections.Generic; +using System.Linq; using System.Xml.Serialization; using DFe.Classes.Assinatura; using DFe.Classes.Flags; @@ -93,34 +95,127 @@ public string ProxyDhRecbto /// public string xMotivo { get; set; } - [XmlElement(ElementName = "cMsg")] - public string ProxyccMsg + /// + /// PR13 - Grupo de mensagens da SEFAZ para o emissor (0-5 ocorrências). + /// + /// O leiaute descreve o PR13 como "Sequência XML": um grupo SEM elemento agrupador, + /// cujos campos aparecem em sequência direta dentro de infProt — PR14 cMsg + /// (ocorrência 0-1, opcional) e PR15 xMsg (ocorrência 1-1, obrigatório). + /// + /// + /// Modelado como coleção de escolha com + /// porque é o único arranjo do XmlSerializer que preserva a ORDEM entre dois nomes + /// de elemento distintos — e é a ordem que permite reconstruir cada ocorrência. + /// Mapear cMsg e xMsg em propriedades escalares, como antes, fazia as + /// ocorrências repetidas colapsarem: só a última sobrevivia. + /// + /// Ver para consumir os pares já reconstruídos. + /// + [XmlElement("cMsg", typeof(string))] + [XmlElement("xMsg", typeof(string))] + [XmlChoiceIdentifier(nameof(MensagensTipos))] + public string[] Mensagens { get; set; } + + /// + /// Discriminador que diz, para cada posição de , se o valor veio + /// de um cMsg ou de um xMsg. Preenchido pelo XmlSerializer; não serializa. + /// + [XmlIgnore] + public TipoMensagemProtocolo[] MensagensTipos { get; set; } + + /// + /// Ocorrências do PR13 já reconstruídas em pares código/mensagem, na ordem do documento. + /// + /// + /// O agrupamento é ancorado no xMsg, que é o campo obrigatório do grupo: cada + /// xMsg encerra uma ocorrência, e o cMsg imediatamente anterior — quando + /// existe — é o código dela. Um cMsg sem xMsg seguinte é ignorado, por não + /// formar ocorrência válida segundo o leiaute. + /// + [XmlIgnore] + public IReadOnlyList Alertas { get { - if (cMsg == null) return null; - return cMsg.Value.ToString(); - } - set - { - if (string.IsNullOrEmpty(value)) + var resultado = new List(); + + if (Mensagens == null || MensagensTipos == null) + return resultado; + + int? codigoPendente = null; + + for (var i = 0; i < Mensagens.Length && i < MensagensTipos.Length; i++) { - cMsg = null; - return; + if (MensagensTipos[i] == TipoMensagemProtocolo.cMsg) + { + codigoPendente = int.TryParse(Mensagens[i], out var codigo) ? codigo : (int?)null; + continue; + } + + resultado.Add(new MensagemProtocolo(codigoPendente, Mensagens[i])); + codigoPendente = null; } - cMsg = int.Parse(value); + + return resultado; } } + /// + /// PR14 - Código da primeira mensagem, quando há. Mantido por compatibilidade com quem + /// consumia o campo escalar; para todas as ocorrências use . + /// [XmlIgnore] - public int? cMsg { get; set; } + public int? cMsg + { + get { return Alertas.Select(a => a.Codigo).FirstOrDefault(c => c.HasValue); } + } - public string xMsg { get; set; } + /// + /// PR15 - Texto da primeira mensagem, quando há. Mantido por compatibilidade com quem + /// consumia o campo escalar; para todas as ocorrências use . + /// + [XmlIgnore] + public string xMsg + { + get { return Alertas.Count > 0 ? Alertas[0].Mensagem : null; } + } /// - /// PR13 - Assinatura XML do grupo identificado pelo atributo “Id” + /// PR90 - Assinatura XML do grupo identificado pelo atributo “Id” /// A decisão de assinar a mensagem fica a critério da UF interessada. /// public Signature Signature { get; set; } } + + /// + /// Discriminador dos campos do grupo PR13. + /// + [XmlType(IncludeInSchema = false)] + public enum TipoMensagemProtocolo + { + cMsg, + xMsg + } + + /// + /// Uma ocorrência do grupo PR13: PR14 cMsg (opcional) e PR15 xMsg. + /// + public class MensagemProtocolo + { + public MensagemProtocolo(int? codigo, string mensagem) + { + Codigo = codigo; + Mensagem = mensagem; + } + + /// + /// PR14 - Código da mensagem. Nulo quando a ocorrência veio sem cMsg. + /// + public int? Codigo { get; private set; } + + /// + /// PR15 - Mensagem da SEFAZ para o emissor. + /// + public string Mensagem { get; private set; } + } } \ No newline at end of file