Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
136 changes: 136 additions & 0 deletions NFCe.Tests/InfProtAlertGroupTests.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Grupo PR13 do protocolo (NT 2018.005, ampliado pela NT 2026.002 §2.3).
/// <para>
/// O leiaute descreve o PR13 como grupo de 0 a 5 ocorrências, sem elemento agrupador,
/// com PR14 <c>cMsg</c> em 0-1 e PR15 <c>xMsg</c> em 1-1. Antes desta mudança
/// <c>infProt</c> mapeava os dois em propriedades escalares, e as ocorrências repetidas
/// colapsavam: só a última sobrevivia à desserialização.
/// </para>
/// </summary>
[TestClass]
public class InfProtAlertGroupTests
{
private const string Namespace = "http://www.portalfiscal.inf.br/nfe";

private static string ProtocoloXml(string ocorrencias)
{
return
"<protNFe xmlns=\"" + Namespace + "\" versao=\"4.00\"><infProt>" +
"<tpAmb>1</tpAmb><verAplic>SVRS</verAplic>" +
"<chNFe>53260446759666000129557280000000511348419115</chNFe>" +
"<dhRecbto>2026-08-08T10:00:00-03:00</dhRecbto>" +
"<nProt>153260000000001</nProt><digVal>abc</digVal>" +
"<cStat>120</cStat><xMotivo>Autorizado o uso da NF-e, com alerta</xMotivo>" +
ocorrencias +
"</infProt></protNFe>";
}

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(
"<cMsg>172</cMsg><xMsg>destinatario bloqueado</xMsg>" +
"<cMsg>999</cMsg><xMsg>segundo alerta</xMsg>" +
"<cMsg>111</cMsg><xMsg>terceiro alerta</xMsg>"));

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(
"<cMsg>172</cMsg><xMsg>com codigo</xMsg>" +
"<xMsg>sem codigo</xMsg>" +
"<cMsg>999</cMsg><xMsg>com codigo de novo</xMsg>"));

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("<cMsg>172</cMsg>"));

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(
"<cMsg>172</cMsg><xMsg>primeira</xMsg>" +
"<cMsg>999</cMsg><xMsg>segunda</xMsg>"));

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(
"<cMsg>172</cMsg><xMsg>com codigo</xMsg>" +
"<xMsg>sem codigo</xMsg>" +
"<cMsg>999</cMsg><xMsg>outro</xMsg>");

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, "<xMsg>").Count);
Assert.AreEqual(2, Regex.Matches(reserializado, "<cMsg>").Count);
}
}
}
123 changes: 109 additions & 14 deletions NFe.Classes/Protocolo/infProt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -93,34 +95,127 @@ public string ProxyDhRecbto
/// </summary>
public string xMotivo { get; set; }

[XmlElement(ElementName = "cMsg")]
public string ProxyccMsg
/// <summary>
/// PR13 - Grupo de mensagens da SEFAZ para o emissor (0-5 ocorrências).
/// <para>
/// 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 <c>cMsg</c>
/// (ocorrência 0-1, opcional) e PR15 <c>xMsg</c> (ocorrência 1-1, obrigatório).
/// </para>
/// <para>
/// Modelado como coleção de escolha com <see cref="XmlChoiceIdentifierAttribute" />
/// 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 <c>cMsg</c> e <c>xMsg</c> em propriedades escalares, como antes, fazia as
/// ocorrências repetidas colapsarem: só a última sobrevivia.
/// </para>
/// Ver <see cref="Alertas" /> para consumir os pares já reconstruídos.
/// </summary>
[XmlElement("cMsg", typeof(string))]
[XmlElement("xMsg", typeof(string))]
[XmlChoiceIdentifier(nameof(MensagensTipos))]
public string[] Mensagens { get; set; }

/// <summary>
/// Discriminador que diz, para cada posição de <see cref="Mensagens" />, se o valor veio
/// de um <c>cMsg</c> ou de um <c>xMsg</c>. Preenchido pelo XmlSerializer; não serializa.
/// </summary>
[XmlIgnore]
public TipoMensagemProtocolo[] MensagensTipos { get; set; }

/// <summary>
/// Ocorrências do PR13 já reconstruídas em pares código/mensagem, na ordem do documento.
/// </summary>
/// <remarks>
/// O agrupamento é ancorado no <c>xMsg</c>, que é o campo obrigatório do grupo: cada
/// <c>xMsg</c> encerra uma ocorrência, e o <c>cMsg</c> imediatamente anterior — quando
/// existe — é o código dela. Um <c>cMsg</c> sem <c>xMsg</c> seguinte é ignorado, por não
/// formar ocorrência válida segundo o leiaute.
/// </remarks>
[XmlIgnore]
public IReadOnlyList<MensagemProtocolo> Alertas
{
get
{
if (cMsg == null) return null;
return cMsg.Value.ToString();
}
set
{
if (string.IsNullOrEmpty(value))
var resultado = new List<MensagemProtocolo>();

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;
}
}

/// <summary>
/// PR14 - Código da primeira mensagem, quando há. Mantido por compatibilidade com quem
/// consumia o campo escalar; para todas as ocorrências use <see cref="Alertas" />.
/// </summary>
[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; }
/// <summary>
/// PR15 - Texto da primeira mensagem, quando há. Mantido por compatibilidade com quem
/// consumia o campo escalar; para todas as ocorrências use <see cref="Alertas" />.
/// </summary>
[XmlIgnore]
public string xMsg
{
get { return Alertas.Count > 0 ? Alertas[0].Mensagem : null; }
}

/// <summary>
/// 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.
/// </summary>
public Signature Signature { get; set; }
}

/// <summary>
/// Discriminador dos campos do grupo PR13.
/// </summary>
[XmlType(IncludeInSchema = false)]
public enum TipoMensagemProtocolo
{
cMsg,
xMsg
}

/// <summary>
/// Uma ocorrência do grupo PR13: PR14 <c>cMsg</c> (opcional) e PR15 <c>xMsg</c>.
/// </summary>
public class MensagemProtocolo
{
public MensagemProtocolo(int? codigo, string mensagem)
{
Codigo = codigo;
Mensagem = mensagem;
}

/// <summary>
/// PR14 - Código da mensagem. Nulo quando a ocorrência veio sem <c>cMsg</c>.
/// </summary>
public int? Codigo { get; private set; }

/// <summary>
/// PR15 - Mensagem da SEFAZ para o emissor.
/// </summary>
public string Mensagem { get; private set; }
}
}