Facilitando um pouco as coisas no Padrão TISS – Parte 2 – o XML
Leonel Fraga de Oliveira 02/07/2015 11:43

No artigo passado vimos um pouco do que é o Padrão TISS e algumas dicas de modelagem de base de dados, e agora vamos a parte mais importante da comunicação entre prestador de saúde e operadora de planos de saúde: a montagem do arquivo XML.

Logo ANS

Cada tipo de guia possui um arquivo próprio, e estes possuem alguns elementos em comum. Na documentação baixada do site da ANS, há arquivos XSD que descrevem as tags, enumerações, tipos de dados e atributos de cada guia.

Uma falta notável é a ausência de documentação oficial mais “amigável” para o desenvolvedor em relação aos arquivos XML, porque saber a estrutura do arquivo através dos XSDs não é nada confortável. Qual a hierarquia das tags? Quais os atributos? Na documentação não encontrei esses dados.

Navegando por aí descobri um site que tem arquivos XML prontos que servem muito bem de exemplo, neles estão todos os dados necessários para enviar guias. Não me lembro da URL para dar os devidos créditos, mas disponibilizo abaixo um arquivo RAR com estes exemplos:

Download: Exemplos de arquivos XML do padrão TISS (34kb)

Vamos nos focar na guia SADT, que por sinal é a mais utilizada nas transações. Faça o download acima, descompacte-o e abra o arquivo ENVIO_LOTE_GUIAS.xml, que contém várias guias SADT para envio em lote. Não vou encher o post com muito código, então acompanhe este artigo com o arquivo XML aberto em outra janela ;).

O arquivo contém as seguintes seções:

- Cabeçalho, onde temos a identificação do tipo de transação, data e hora e sequencial, origem das informações (prestador de serviço) e destino das informações (operadora de plano de saúde), e por último a versão do TISS utilizada.

- Corpo da mensagem TISS, entre as tags prestadorParaOperadora, loteGuias (início do lote), guiasTISS, e cada guia representada pela tag guiaSP-SADT, que ocorre N vezes em um lote.

- Epílogo, onde vai um código hash que é comparado pela operadora de serviços de saúde para verificar alterações no arquivo.

Ao montar o XML, preste muita atenção na ordem das tags, principalmente as de cada guia, pois se alguma coisa ficar faltando ou em ordem trocada o validador vai chiar.

Para montar o XML com os dados temos algumas opções com o .NET Framework (ou outra ferramenta que você queira): construir as classes com a ferramenta xsd.exe, montar o arquivo programaticamente através delas e serializar um objeto para obter o arquivo texto; utilizar a classe XmlDocument e montando tag a tag; ou a mais radical, mais “burra” porém simples, fácil de depurar e que eu adotei: utilizando um StringBuilder e concatenando as tags com os valores (usando o Format, claro). Uma recomendação é tirar os acentos das strings que serão os valores das tags, e valores decimais devem ser formatados com o separador decimal ponto (.).

Documentos devem ser informados sem máscara (traços e pontos), e o formato de data… bem, esse é um ponto BEM chato, pois depende da operadora de saúde. O padrão que eu uso é “yyyy-MM-dd”, mas dependendo da operadora pode ser outro. Portanto, se for fazer um sistema multi-operadoras, nada de deixar esse formato hard-coded.

Algo assim:

StringBuilder sbXML = new StringBuilder();
//Cabeçalho do Lote
sbXML.AppendLine("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>");
sbXML.AppendLine("<ans:mensagemTISS xmlns:ans=\"http://www.ans.gov.br/padroes/tiss/schemas\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.ans.gov.br/padroes/tiss/schemas http://www.ans.gov.br/padroes/tiss/schemas/tissV3_01_00.xsd\">");
sbXML.AppendLine("  <ans:cabecalho>");
sbXML.AppendLine("      <ans:identificacaoTransacao>");
sbXML.AppendLine("          <ans:tipoTransacao>ENVIO_LOTE_GUIAS</ans:tipoTransacao>");
sbXML.AppendLine(String.Format("                <ans:sequencialTransacao>{0}</ans:sequencialTransacao>", lote.NumeroLote));
sbXML.AppendLine(String.Format("                <ans:dataRegistroTransacao>{0}</ans:dataRegistroTransacao>", dtTrans.ToString(_formatoData)));
sbXML.AppendLine(String.Format("                <ans:horaRegistroTransacao>{0}</ans:horaRegistroTransacao>", dtTrans.ToString("HH:mm:ss")));
sbXML.AppendLine("      </ans:identificacaoTransacao>");
sbXML.AppendLine("      <ans:origem>");
sbXML.AppendLine("          <ans:identificacaoPrestador>");
sbXML.AppendLine(String.Format("                <ans:codigoPrestadorNaOperadora>{0}</ans:codigoPrestadorNaOperadora>", codPrestOp));
sbXML.AppendLine("          </ans:identificacaoPrestador>");
sbXML.AppendLine("      </ans:origem>");
sbXML.AppendLine("      <ans:destino>");
sbXML.AppendLine(String.Format("            <ans:registroANS>{0}</ans:registroANS>", conv.CodigoANS.Replace(".", "").Replace("-", "")));
sbXML.AppendLine("      </ans:destino>");
sbXML.AppendLine("      <ans:versaoPadrao>3.02.00</ans:versaoPadrao>");
sbXML.AppendLine("  </ans:cabecalho>");
sbXML.AppendLine("  <ans:prestadorParaOperadora>");
sbXML.AppendLine("      <ans:loteGuias>");
sbXML.AppendLine(String.Format("            <ans:numeroLote>{0}</ans:numeroLote>", lote.NumeroLote));
sbXML.AppendLine("          <ans:guiasTISS>");
//Guias
using (ADODbHelper db = new ADODbHelper())
{
	using (DataTable dt = db.GetTable("select TipoGuia, IdGuia from GuiasLote where LoteId = @ID", new DbParameter[1] { db.CreateInputParameter("@ID", idLote) }))
	{
		foreach (DataRow r in dt.Rows)
		{
			if (r["TipoGuia"].ToString() == "Resumo de Internação")
			{
				sbXML.AppendLine(GeraXMLResumoInternacao((int)r["Idguia"]));
			}
			else if (r["TipoGuia"].ToString() == "SADT")
			{
				sbXML.AppendLine(GeraXMLGuiaSADT((int)r["Idguia"]));
			}
		}
	}
}

sbXML.AppendLine("          </ans:guiasTISS>");
sbXML.AppendLine("      </ans:loteGuias>");
sbXML.AppendLine("  </ans:prestadorParaOperadora>");
sbXML.AppendLine("</ans:mensagemTISS>");

Embora eu tenha funções que escrevem tanto as guias SP/SADT e Resumo de Internação, o arquivo XML deve ter apenas um tipo de guia por vez.

Você notou que eu não coloquei a seção epilogo, fechando a tag <mensagemTISS> logo após o </prestadorParaOperadora> desse arquivo que eu montei?

É nessa tag <epilogo> que vai o código hash de verificação, e o cálculo deste hash que dá mais dor de cabeça a nós desenvolvedores. O código hash nada mais é do que o hash MD5 dos valores de todas as tags concatenados. Sendo assim, devemos ler cada tag recursivamente, concatenando os valores internos (valores, não atributos ;)) em uma string, calcular o hash MD5 desta string e acrescentar a tag <epilogo> com o hash calculado. Simples, né?

Já que a missão aqui é facilitar, vamos usar as classes XElement e XmlDocument para remover nós vazios, remover espaços e padronizar os valores para letra maiúscula.

Limpando o arquivo:

//Elimina nós vazios, remove espaços (início e fim) dos valores e transforma em maiúsculos no XML
XElement docx = XElement.Parse(sbXML.ToString());
docx.Descendants().Where(e => String.IsNullOrEmpty(e.Value)).Remove();
docx.TrimWhiteSpaceFromValues();
docx.UpperValues();

//Recarrega o XML tratado no stringbuilder de origem, para gerar o hash.
using (MemoryStream ms = new MemoryStream())
{
	docx.Save(ms);
	ms.Position = 0;
	using (StreamReader sr = new StreamReader(ms))
	{
		sbXML = new StringBuilder(sr.ReadToEnd());
	}
}

Mas pera aê… os métodos TrimWhiteSpaceFromValues (tira espaços) e UpperValues (transforma em maiúsculas) NÃO são métodos nativos da classe XElement, né? Pois é, estes são dois métodos de extensão que foram criados para facilitar as coisas. Abaixo temos os métodos de extensão para remover espaços e transformar em maiúsculas os valores dentro de um XElement:

public static class XElementExtensions
{
	/// <summary>
	/// Trims whitespace from the xml node values.  
	/// DOES NOT trim whitespace outside of values, can use PreserveWhitespace LoadOption when parsing for that.
	/// </summary>
	/// <param name="element"></param>
	public static void TrimWhiteSpaceFromValues(this XElement element)
	{
		foreach (var descendent in element.Descendants())
		{
			if (!descendent.HasElements)
			{
				descendent.SetValue(descendent.Value.Trim());
			}
			else
			{
				descendent.TrimWhiteSpaceFromValues();
			}
		}
	}

	public static void UpperValues(this XElement element)
	{
		foreach (var descendent in element.Descendants())
		{
			if (!descendent.HasElements)
			{
				descendent.SetValue(descendent.Value.ToUpper());
			}
			else
			{
				descendent.UpperValues();
			}
		}
	}

}

Depois do arquivo preparado, vamos por fim calcular o hash. Veja o código abaixo:

//Recarrega o XML tratado no stringbuilder de origem, para gerar o hash.
using (MemoryStream ms = new MemoryStream())
{
	docx.Save(ms);
	ms.Position = 0;
	using (StreamReader sr = new StreamReader(ms))
	{
		sbXML = new StringBuilder(sr.ReadToEnd());
	}
}

//Hash
XmlDocument doc = new XmlDocument();
doc.LoadXml(sbXML.ToString());

string x = doc.InnerText;
byte[] hash = FuncoesGeraisDL.HashMD5(doc.InnerText); //InnerText retorna todos os valores concatenados, que é usado para calcular o hash MD5
string strHash = BitConverter.ToString(hash).Replace("-","");

Recarregamos o nosso XML tratado de volta ao StringBuilder em que ele foi gerado, e em seguida instanciamos um XmlDocument com o conteúdo desse StringBuilder. Em vez de fazer uma leitura recursiva como eu sugeri acima, para obter somente o texto dos valores, sem as tags, e já concatenados do jeito que queremos basta ler a propriedade InnerText da nossa classe XmlDocument. Só isso? Sim, só isso!

O cálculo do MD5 fazemos através da classe MD5CryptoServiceProvider, porém no código acima este processo está encapsulado no método FuncoesGerais.HashMD5(string) e o resultado é um array de bytes com o hash calculado.

Este hash calculado é colocado no arquivo texto como uma string hexadecimal, em vez do Base64 normalmente utilizado. Para converter o array de byte para uma string hexa, utilizamos a classe BitConverter, passando o array para o método estático ToString() desta, e por último retirando os traços.

Agora só falta colocar o hash no StringBuilder que resultará no arquivo texto, e gerá-lo, certo? Então veja abaixo:

StringBuilder tagHash = new StringBuilder();
tagHash.AppendLine("  <ans:epilogo>");
tagHash.AppendLine(String.Format("      <ans:hash>{0}</ans:hash>", strHash));
tagHash.AppendLine("  </ans:epilogo>");
tagHash.AppendLine("</ans:mensagemTISS>");

//Finalização
sbXML.Replace("</ans:mensagemTISS>", tagHash.ToString());

doc.LoadXml(sbXML.ToString()); //Carrega o documento para mostrar "bem-formatado" para o usuário.


this.NomeArquivo = String.Format("{0}-{1:dd-MM-yyyy}.xml", FuncoesGeraisDL.tira_acentos(conv.NomeFantasia).Replace(" ","_"), DateTime.Now);

using (MemoryStream ms = new MemoryStream())
{
	doc.Save(ms);
	ms.Position = 0;
	using (StreamReader sr = new StreamReader(ms))
	{
		return sr.ReadToEnd();
	}
}

Neste caso montei em outro StringBuilder a tag <ans:epilogo> (notem que coloquei novamente o </ans:mensagemTISS>, e após isso simplesmente de um Replace no texto </ans:mensagemTISS> do StringBuilder com o XML com a tag <ans:epilogo> do StringBuilder tagHash completa! Depois carreguei o StringBuilder no nosso XmlDocument somente para apresentar ele formatado para o usuário e retornei o conteúdo dele através de um MemoryStream. Depois disso você pode salvar no disco, ou se a aplicação for Web disponibilizar o conteúdo para download.

Com isso “matamos” a parte mais chata da coisa, mas ainda não acabou. No próximo artigo faremos uma pré-validação do arquivo XML utilizando a validação que as operadoras utilizam, antes de enviar o arquivo.

Leonel Fraga de Oliveira Leonel Fraga de Oliveira é formado em Processamento de Dados na Faculdade de Tecnologia de São Paulo (FATEC-SP - 2002) e anteriormente em Técnico em Eletrônica, pela ETE Professor Aprígio Gonzaga (lá em 1999).
Atualmente trabalha como Analista de Sistemas na Prefeitura Municipal de São Caetano do Sul - SP
Tem como hobbies DJing (também trabalha como DJ freelancer) e ciclismo, além da manutenção dos sites NeoMatrix Light e NeoMatrix Tech.
Gosta de música eletrônica, tecnologia, cinema (super fã de Jornada nas Estrelas), gastronomia e outras coisas mais.


Compartilhe nas redes sociais

   

Deixe seu comentário

comments powered by Disqus