Technical Article

Validando Notas Fiscais Eletrônicas: veraPDF e Mustang no Delphi

Uma nota fiscal Factur-X ou ZUGFeRD é composta por dois documentos em um único nome de arquivo. O documento externo é um contêiner PDF/A-3 que um leitor de arquivamento precisa aceitar pelos próximos dez anos. O documento interno é um XML de nota fiscal que o sistema contábil do comprador precisa processar em conformidade com o EN 16931. O erro que coloca notas fiscais defeituosas em produção é acreditar que acertar o primeiro garante automaticamente o segundo. Não garante. Um arquivo pode ser um PDF/A-3 impecável e ainda assim conter um XML que nenhuma autoridade fiscal aceitará, e pode conter um XML perfeito do EN 16931 dentro de um contêiner que falha na validação de arquivamento. As duas camadas são validadas por duas ferramentas diferentes que nada sabem uma sobre a outra, e um pipeline real precisa satisfazer ambas

Dois validadores, duas perguntas diferentes

O veraPDF é a implementação de referência para PDF/A. Aponte-o para uma nota fiscal e ele responde a uma pergunta: este é um arquivo PDF/A-3 em conformidade? Ele verifica o que a ISO 19005-3 exige. Se cada fonte está incorporada. Se existe um OutputIntent. Se os metadados XMP declaram a parte e o nível de conformidade corretos. Para uma nota fiscal eletrônica, ele também verifica o encanamento de arquivo associado que o PDF/A-3 requer, porque o XML acompanha como um arquivo incorporado com um /AFRelationship e uma entrada no array /AF do catálogo do documento. O veraPDF não diz nada sobre se o total da nota fiscal está correto, pois isso não faz parte de sua competência

O Mustang é o validador de código aberto do Mustangproject. Ele responde à pergunta ortogonal: o XML incorporado é uma nota fiscal válida? Ele executa o XML contra o esquema do perfil declarado e então aplica as regras de negócio do EN 16931 e os conjuntos de regras específicos de cada país sobrepostos, incluindo o CIUS do XRechnung. Ele verifica se um identificador de IVA do vendedor está presente quando os totais o exigem, se os valores de descontos e encargos reconciliam com o total do documento, se o URN de perfil no XML corresponde ao que o arquivo afirma ser. O Mustang não se importa se o PDF ao redor incorpora suas fontes, pois esse é o trabalho do veraPDF

Nenhuma ferramenta é um superconjunto da outra. O veraPDF aprova um contêiner estruturalmente perfeito em torno de um XML sem sentido. O Mustang aprova um XML perfeito embrulhado em um contêiner sem OutputIntent. Cada um detecta exatamente a classe de defeito que o outro não enxerga, e é exatamente por isso que um conjunto de validação sério executa ambos e trata um arquivo como pronto para envio somente quando os dois concordam

A matriz de validação

Para provar que a biblioteca produz arquivos que sobrevivem a ambas as etapas, o conjunto de testes monta uma matriz. Seis perfis de nota fiscal cobrem o intervalo que um pipeline europeu encontra na prática: Factur-X EN 16931, Factur-X BASIC, a variante francesa B2B Factur-X EXTENDED, XRechnung 3.0, ZUGFeRD 1.0 COMFORT e ZUGFeRD 2.0 BASIC. Cada perfil é gerado em dois níveis de subconformidade do PDF/A, 3b e 3u, porque os requisitos dos níveis B e U divergem no mapeamento Unicode e um arquivo que passa em um pode falhar no outro. Seis perfis vezes dois níveis totalizam doze arquivos, todos gerados sem interface gráfica pelo mesmo caminho de código que o exemplo com GUI utiliza, portanto os artefatos testados não são ajustados manualmente para o teste

O gerador grava todos os doze e um script alimenta cada um aos dois validadores. Na primeira execução completa, o veraPDF aprovou todos os doze. O encanamento do contêiner estava correto em todos: arquivos associados registrados, conformidade XMP declarada, intenções de saída presentes. O Mustang aprovou oito. Quatro notas fiscais eram arquivos PDF/A-3 estruturalmente válidos contendo XML que o validador de regras de negócio rejeitou, que é precisamente a divisão que a abordagem de duas ferramentas existe para revelar. Se o conjunto de testes tivesse confiado somente no veraPDF, esses quatro pareceriam concluídos

As duas correções que fecharam a lacuna

As quatro falhas do Mustang vieram de duas causas distintas, e a correção de cada uma é um detalhe que vale saber antes de você gerar esses perfis

A primeira foi o perfil Factur-X EXTENDED France B2B. O gerador original passou um rótulo interno como nível de conformidade e um URN interno como diretriz, e o Mustang rejeitou o arquivo com um erro de valor de conformidade inválido seguido de um erro de tipo de perfil não suportado. O motivo é que o campo XMP fx:ConformanceLevel não é um campo de texto livre para sua própria nomenclatura de perfil. O Factur-X define exatamente cinco valores padrão para ele: MINIMUM, BASIC WL, BASIC, EN 16931 e EXTENDED. Uma nota fiscal específica para o mercado B2B francês ainda é um documento do perfil EXTENDED no que diz respeito aos metadados XMP. O caráter francês da nota fiscal não é expresso criando um sexto valor de conformidade. É expresso pelo código de país, FR, e pelo identificador de diretriz dentro do XML, que precisa carregar o prefixo urn:cen.eu:en16931:2017#conformant# que marca um CIUS em conformidade com o EN 16931. Passar o valor padrão EXTENDED com FR como código de país e o URN de diretriz correto tornou o arquivo em conformidade

Na API da biblioteca, isso é uma chamada a AddFacturXAssociatedFileFromString com a conformidade, o país e a diretriz alinhados. O argumento do nível de conformidade carrega o token padrão, o argumento do código de país carrega FR, e o URN de diretriz reside nos bytes XML que você passa

var
  FileID: Integer;
begin
  PDF.SetPDFAMode(5);            // PDF/A-3b
  PDF.NewDocument;
  // ... draw the human-readable invoice page ...
  // ExtendedXML carries an EN 16931 guideline URN of the form
  //   urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
  FileID := PDF.AddFacturXAssociatedFileFromString(
    ExtendedXML,
    'EXTENDED',          // standard fx:ConformanceLevel, not an internal label
    'factur-x.xml',
    'Factur-X EXTENDED invoice',
    'Alternative',       // /AFRelationship
    '1.0',
    'FR');               // France B2B marked by country code, not by conformance
  if FileID = 0 then
    raise Exception.Create('Factur-X attachment rejected');
  PDF.SaveToFile('02_Factur-X-EXTENDED-FR_PDFA-3b.pdf');
end;

A segunda causa foi o perfil ZUGFeRD 1.0 COMFORT, e não teve nada a ver com metadados. O ZUGFeRD 1.0 é validado contra o XSD :1p0, que é mais rigoroso em cardinalidade do que os resumos em prosa sugerem. O XSD exige que o sumário monetário do acerto do cabeçalho, ram:SpecifiedTradeSettlementMonetarySummation, contenha ram:ChargeTotalAmount e ram:AllowanceTotalAmount exatamente uma vez cada. O XML gerado omitiu ambos, portanto o Mustang informou que os elementos devem ocorrer exatamente uma vez. Eles não são opcionais quando o esquema diz que minOccurs é um. Emitir ambos na ordem de sequência do XSD, imediatamente após ram:LineTotalAmount, com um valor de 0.00 quando não há encargos ou descontos, satisfez o esquema. Um zero é um elemento presente; um elemento ausente é uma violação do esquema. Com essas duas correções aplicadas, a matriz passou para doze de doze no Mustang, mantendo doze de doze no veraPDF

Os campos XRechnung que convertem inválido em válido

O XRechnung merece uma nota própria porque seu CIUS alemão adiciona regras de negócio ausentes do conjunto base do EN 16931, e elas falham de maneiras que parecem não haver nada de errado com o documento à primeira vista. Dois deles dizem respeito a endereços eletrônicos. BT-34 é o endereço eletrônico do vendedor e BT-49 é o endereço eletrônico do comprador, os endpoints de roteamento que um portal do setor público alemão usa para entregar e confirmar o recebimento da nota fiscal. O modelo base do EN 16931 os trata como opcionais. O XRechnung não. Omita qualquer um e a nota fiscal está bem formada, válida contra o esquema e rejeitada

O terceiro é a regra BR-DE-6, que exige a presença do número de telefone de contato do vendedor. É o tipo de campo que um desenvolvedor descarta porque parece mais apresentação do que dado, e sua ausência produz uma falha de validação que aponta para o grupo de contato do vendedor em vez de para algo obviamente ausente. Fornecer BT-34, BT-49 e o número de telefone do vendedor é o que move um arquivo XRechnung de inválido para válido no Mustang, e nenhum desses três muda nada que o veraPDF veja, porque todos os três residem no XML

Conectando a saída da biblioteca a um validador

O ponto arquitetural por trás do conjunto de testes se generaliza para qualquer sistema de negócios. A biblioteca PDF escreve um contêiner em conformidade e incorpora o XML. Ela não deve, nem deveria, tentar ser a autoridade de regras de negócio do EN 16931. O ValidateFacturXInvoice na biblioteca verifica a consistência do contêiner - se o array /AF do catálogo, a árvore de nomes de arquivos incorporados, o DocumentFileName XMP, o perfil, a diretriz e o /AFRelationship estão todos de acordo - mas não valida códigos fiscais nem reconcilia valores. A divisão correta de trabalho é o sistema de negócios extrair o XML e entregá-lo a um validador de nota fiscal dedicado, exatamente como o conjunto de testes o entrega ao Mustang

Ler o arquivo de volta informa o que foi realmente escrito. DetectFacturXInvoice informa se uma nota fiscal foi reconhecida, e GetFacturXInvoiceInfo lê os campos de metadados por tag: a tag 1 é o nome do arquivo incorporado, a tag 2 é o DocumentFileName XMP, a tag 5 é o nível de conformidade, a tag 6 é o identificador de diretriz e a tag 7 é o /AFRelationship. Confirmar que o nível de conformidade lido de volta é o token padrão e não um rótulo interno é a maneira mais barata de detectar o erro EXTENDED antes de um arquivo sair do seu build

function ExtractAndInspect(const PdfPath: string): AnsiString;
var
  Profile, Guideline: WideString;
begin
  Result := '';
  PDF.LoadFromFile(PdfPath);
  if PDF.DetectFacturXInvoice = 1 then
  begin
    Profile   := PDF.GetFacturXInvoiceInfo(5);  // fx:ConformanceLevel
    Guideline := PDF.GetFacturXInvoiceInfo(6);  // XML guideline ID
    Writeln('Profile:   ', Profile);
    Writeln('Guideline: ', Guideline);
    // Hand the raw XML to a dedicated EN 16931 / Mustang validator.
    Result := PDF.ExtractFacturXXMLToString;
  end;
end;

ExtractFacturXXMLToString retorna os bytes XML brutos como uma AnsiString, prontos para serem gravados em um arquivo ou transmitidos para um processo validador. No conjunto de testes, esse destino é o Mustang, invocado por meio de seu jar de linha de comando, com o veraPDF executado na mesma passagem sobre o mesmo arquivo. A ligação é pequena: um gerador de console, EInvoiceValidation.dpr, grava os doze arquivos usando o modelo de nota fiscal compartilhado do exemplo, e um script, run-validation.ps1, conduz ambos os validadores sobre o diretório de saída e imprime uma tabela de aprovação e reprovação. O mesmo formato em duas etapas - gerar com a biblioteca e verificar com validadores externos - é o que um trabalho de integração contínua deve executar em cada alteração na geração de notas fiscais, porque a única maneira de saber se um arquivo satisfaz ambas as camadas é perguntar às duas ferramentas

Se o seu pipeline também precisa certificar o contêiner antes de assinar, o lado de preflight deste trabalho é abordado em nosso guia de preflight de PDF/A e PDF/UA no Delphi, e o fluxo mais amplo de certificar e depois assinar é descrito em a bancada de conformidade e assinatura. Ambos se baseiam no mesmo caminho de geração que acompanha a Biblioteca Delphi PDF para Delphi e C++Builder, ao lado das APIs de PDF/A, arquivo associado e metadados usadas aqui