Artigo Técnico

Validação nativa de faturas EN 16931 no Delphi sem Java

O PDFlibPas, a biblioteca de PDF Delphi da losLab, valida regras de negócio de EN 16931, XRechnung 3.0.2 e Peppol BIS Billing 3.0 nativamente em Pascal. O TPDFlib.ValidateInvoiceBusinessRulesBuiltIn lê o XML embutido num PDF Factur-X ou ZUGFeRD e devolve um relatório JSON com os IDs oficiais das regras, sem runtime Java, sem Saxon e sem uma toolchain Schematron. O relatório se rotula como subset, e esse rótulo é o campo mais importante dele

Pipeline da validação embutida de faturas EN 16931 na biblioteca de PDF PDFlibPas para Delphi: detecção Factur-X, etapas de schema e schematron e um relatório JSON de subset coberto
o validador embutido roda as etapas de schema e schematron in process e informa exatamente quais regras o subset cobre e quais não

A pressão por trás disso vem em grande parte da Alemanha. Desde janeiro de 2025 toda empresa na Alemanha tem de ser capaz de receber faturas eletrônicas B2B estruturadas nacionais, e a obrigação de emitir chega em etapas. De repente, o lado receptor de uma aplicação Delphi de contabilidade ou ERP precisa responder a uma pergunta que nunca fez antes: o XML dentro deste PDF é de fato uma fatura EN 16931 válida? A resposta oficial vem de artefatos Schematron compilados para XSLT 2.0, ou do validador KoSIT para XRechnung, e ambos rodam na JVM. Distribuir e atualizar um JRE ao lado de cada instalação desktop só para rejeitar uma fatura ruim é um custo que muita equipe prefere não carregar

O que o validador embutido realmente valida?

O validador nativo mapeia o XML da fatura nos termos de negócio do EN 16931 e roda um subset documentado das regras dos artefatos de validação EN 16931 1.3.16, reportando cada achado sob o seu ID oficial de regra. As duas sintaxes são suportadas: UN/CEFACT CII D16B, que é o que Factur-X e ZUGFeRD embutem, e OASIS UBL 2.1 Invoice e CreditNote. As áreas cobertas são:

  • Termos obrigatórios: identificador de especificação, número da fatura, data de emissão, código de tipo, moeda, nomes de vendedor e comprador, endereços, totais do documento, detalhamento de VAT e linhas (BR-01 a BR-16, BR-21 a BR-28, BR-CO-04, BR-CO-18, BR-CO-26, BR-45 a BR-48)
  • Cálculos: totais de linha, desconto, encargo, imposto e a pagar, mais a base tributável e o valor do imposto de cada categoria de VAT (BR-CO-10 a BR-CO-17, BR-xx-08, BR-xx-09)
  • Regras de categoria de VAT para S, Z, E, AE, K, G, O, L e M: alíquotas de linha, razões de isenção e os identificadores de VAT que cada categoria exige
  • Todas as listas de códigos BR-CL-01 a BR-CL-26, incluindo os mais de 2100 códigos de unidade UN/ECE Rec 20/21 por trás do BR-CL-23, e as regras de duas casas decimais BR-DEC-01, -05, -09 a -20 e -23
  • Regras de perfil: XRechnung 3.0.2 (BR-DE-1 a BR-DE-31, BR-DEX-01 a BR-DEX-14) em CII e UBL, e as regras core, comuns, de listas de códigos e nacionais do Peppol BIS Billing 3.0.20 em UBL

O escopo é reportado, não implícito. Sem um arquivo de schema, a etapa de schema confirma a boa formação e o elemento raiz. Cada etapa lista as regras fora do subset num campo notCovered, e o GetBuiltInInvoiceRuleCatalog devolve o mapa completo em JSON, para a sua UI mostrar ao usuário exatamente o que um resultado verde cobre

Outro limite é fácil de perder. Documentos Factur-X MINIMUM e BASIC WL não são faturas EN 16931, então o validador não aplica regras EN 16931 a eles. Em vez disso ele adiciona uma ocorrência informativa de BuiltIn.Coverage.Profile dizendo isso, que é a resposta honesta e também uma dica: sob as regras alemãs esses dois perfis não contam como faturas eletrônicas de qualquer forma

Validando o XML embutido de um PDF híbrido

O ValidateInvoiceBusinessRulesBuiltIn recebe um argumento, o nome do arquivo embutido, e uma string vazia é a escolha normal. O PDFlibPas então escolhe o anexo que a detecção Factur-X identificar; para um PDF sem metadados Factur-X ele recua para o único anexo .xml. Vários anexos .xml candidatos, ou dois anexos com o nome solicitado, produzem um erro Invoice.AttachmentAmbiguous em vez de um chute, a decodificação é limitada a 64 MB, e uma fatura ausente coloca LastErrorCode em 401

uses
  System.SysUtils, System.JSON, PDFlibrary;

procedure CheckInboundInvoice(const FileName: string);
var
  PDF: TPDFlib;
  Report: TJSONObject;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile(FileName, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s (error %d)',
        [FileName, PDF.LastErrorCode]);
    // Nome vazio: valida o anexo Factur-X / ZUGFeRD detectado
    Report := TJSONObject.ParseJSONValue(
      PDF.ValidateInvoiceBusinessRulesBuiltIn('')) as TJSONObject;
    try
      Writeln('profile:  ', Report.GetValue<string>('profile'));
      Writeln('valid:    ', Report.GetValue<Boolean>('valid'));
      Writeln('coverage: ', Report.GetValue<string>('coverage'));        // "subset"
      Writeln('full:     ', Report.GetValue<Boolean>('fullConformance')); // nunca True aqui
    finally
      Report.Free;
    end;
  finally
    PDF.Free;
  end;
end;

O nível superior do relatório traz standard, version, profile, complete, valid, fullConformance, coverage, uma etapa schema e uma schematron, e um objeto package que é null no modo embutido. Cada etapa reporta um status, o engine, o número de regras executadas, a cobertura própria e um array issues. Toda ocorrência tem ruleID, fieldPath, severity, category, line e message

Lendo ocorrências pela categoria em vez da mensagem

O campo category é o que torna o relatório utilizável numa caixa de entrada real, porque diz quem precisa corrigir o problema. Os valores são syntax, schema, cardinality, calculation, vat, codelist, decimal, profile, coverage, processing e integrity. Um achado de calculation geralmente aponta para o código de arredondamento do remetente, um de codelist para os dados mestre, e um de profile para um campo XRechnung como a buyer reference

procedure ListIssues(Report: TJSONObject);
var
  Item: TJSONValue;
  Issue: TJSONObject;
begin
  for Item in Report.GetValue<TJSONArray>('schematron.issues') do
  begin
    Issue := Item as TJSONObject;
    if Issue.GetValue<string>('severity') = 'info' then
      Continue;  // notas de coverage como BuiltIn.Coverage.Profile
    Writeln(Format('[%s] %s at %s: %s', [
      Issue.GetValue<string>('category'),
      Issue.GetValue<string>('ruleID'),
      Issue.GetValue<string>('fieldPath'),
      Issue.GetValue<string>('message')]));
  end;
end;

Rode o mesmo loop sobre schema.issues também, porque falhas de boa formação e de elemento raiz caem lá e a etapa de regras de negócio nunca vê um documento que não conseguiu parsear

Por que a mesma fatura passa em CII e falha em UBL?

Os artefatos oficiais não aplicam a mesma tolerância numérica nas duas sintaxes, e o PDFlibPas copia cada regra do XSLT da sua própria sintaxe em vez de harmonizá-las. O BR-CO-17 é o caso mais claro. Ele confere se o valor do imposto da categoria de VAT equivale à base tributável vezes a alíquota, arredondado a duas casas decimais, e as duas sintaxes permitem uma diferença de uma unidade de moeda. O artefato CII escreve isso como <= e >=; o artefato UBL usa abs com um < estrito

Pegue uma base tributável de 100.00 a 19 por cento, então o imposto esperado é 19.00, e uma fatura que declara 20.00. Em CII, 20.00 menos 1 dá 19.00, o que satisfaz o <=, e o BR-CO-17 passa. Em UBL a mesma comparação é estrita, 19.00 não é menor que 19.00, e o BR-CO-17 falha. Ninguém envia essa fatura de propósito, mas geradores com um bug de arredondamento ficam exatamente sobre esses limites, e uma suíte de testes que só cobre uma sintaxe não vai notar

Comparação de tolerância do BR-CO-17 na validação de faturas do PDFlibPas para Delphi: a mesma fatura com uma diferença de arredondamento de uma unidade passa em CII e falha em UBL
uma unidade de arredondamento cai exatamente no limite do BR-CO-17, então a mesma fatura passa em CII e falha em UBL

As diferenças vão além do BR-CO-17. O BR-xx-08 exige igualdade exata após arredondamento a duas casas decimais em CII, enquanto o UBL permite ±1 e só dispara quando a categoria realmente tem linhas, descontos ou encargos. O BR-O-08 é exato nas duas. O artefato CII define BR-AF-09 e BR-AG-09 como true(), então eles nunca falham ali, enquanto o UBL os confere com ±1. E quando um detalhamento de VAT em CII não tem alíquota nenhuma, as regras de categoria não O passam vacuamente porque o XPath every $rate sobre uma sequência vazia é verdadeiro. O código nativo precisa pular do mesmo jeito, ou ele reporta erros em arquivos de exemplo oficiais

Uma segunda armadilha envolve IDs de regra. O prefixo de categoria da regra não é o código da categoria de VAT: K mapeia para BR-IC, L para BR-AF e M para BR-AG. Derivar o ID a partir do código da categoria produz BR-K-09 e BR-K-10, IDs que não existem no EN 16931. Se a sua aplicação mapeia IDs de regra para textos de ajuda, pegue o ID do relatório e nunca construa a partir do código da categoria de imposto

Como as regras de XRechnung e Peppol são ativadas?

O validador embutido seleciona regras de perfil a partir do identificador de especificação (BT-24). Um identificador contendo xrechnung roda as regras CIUS do XRechnung 3.0.2 em CII ou UBL; o identificador de extensão urn:xeinkauf.de:kosit:extension:xrechnung_3.0 ativa também as regras BR-DEX. As verificações de valor de linha de subfatura e de VAT (BR-DEX-02, -03 e -09 a -14) rodam só em UBL; em CII, o BR-DEX-15 reporta que linhas de subfatura não estão disponíveis. Um identificador contendo peppol roda as regras do Peppol BIS Billing 3.0.20, que o Peppol define apenas para UBL. Um documento CII alegando uma customização Peppol recebe uma nota informativa de BuiltIn.Coverage.Profile dizendo que aquelas regras não foram verificadas, em vez de uma passagem silenciosa. Para outros perfis CIUS, use os artefatos oficiais

Um detalhe de UBL vale a pena saber: o BR-06 e o BR-07 aceitam só PartyLegalEntity/RegistrationName como nome de vendedor ou comprador. Recuar para PartyName/Name parece generoso mas esconde falhas reais de BR-06, e é por isso que a verificação é estrita

Quando você ainda precisa do Schematron oficial?

Use os artefatos oficiais, o Schematron EN 16931 via Saxon ou o validador KoSIT para XRechnung, sempre que um resultado tiver peso legal ou contratual: rejeitar a fatura de um fornecedor, certificar o seu próprio gerador ou resolver uma disputa. O relatório embutido nunca coloca fullConformance em true. Essa flag exige um pacote de regras verificado, as duas etapas passadas, as duas etapas rodadas por um engine executando as próprias regras do pacote e nada sobrando no notCovered

O ValidateInvoiceBusinessRulesWithPackage roda esse caminho. Um pacote de regras é um diretório com um manifest rulepackage.xml, escrito pelo CreateInvoiceRulePackage, que nomeia o padrão, a versão, o schema e a stylesheet Schematron e registra o SHA-256 de cada arquivo. O PDFlibPas confere cada digest quando o pacote carrega e de novo imediatamente antes de um engine ler o arquivo, o que estreita a janela para uma troca após a verificação. Os caminhos não podem sair do diretório do pacote, e o loader XSD nativo recusa imports que o manifest não lista. Com o xmllint configurado, os schemas importados são lidos pelo próprio xmllint, então só a verificação de carga os protege

const
  // SHA-256 do rulepackage.xml que você revisou e aprovou
  ApprovedManifestSHA256 = '<64 hex digits from your release review>';
var
  Report, Pkg: TJSONObject;
begin
  // PDF: um TPDFlib com a fatura já carregada, como no primeiro exemplo
  Report := TJSONObject.ParseJSONValue(
    PDF.ValidateInvoiceBusinessRulesWithPackage('',
      'C:\Rules\EN16931-CII-1.3.16', ApprovedManifestSHA256,
      '',                          // vazio: etapa XSD nativa
      'C:\Java\bin\java.exe', 'C:\Saxon\saxon-he.jar',
      0)) as TJSONObject;          // 0: 30000 ms por tool
  try
    if not (Report.FindValue('package') is TJSONObject) then
      raise Exception.Create('No rule package in the report');
    Pkg := Report.GetValue<TJSONObject>('package');
    if not (Pkg.GetValue<Boolean>('pinned') and Pkg.GetValue<Boolean>('verified')) then
      raise Exception.Create('Rule package failed its integrity check');
    if Report.GetValue<Boolean>('fullConformance') then
      Writeln('Conforms to the complete rule package');
  finally
    Report.Free;
  end;
end;

O digest fixado não é enfeite opcional. Os digests de arquivo provam que os arquivos batem com o manifest, mas nada dentro de um pacote prova que o próprio manifest é genuíno: um atacante capaz de reescrever as regras pode reescrever o manifest junto. Só um digest fixado pelo chamador, no código ou numa configuração assinada, fecha essa lacuna, e o relatório mantém pinned e verified separados para que você saiba qual garantia realmente tem

Fluxo de integridade de um pacote de regras de fatura PDFlibPas no Delphi: digests SHA-256 do manifest reavaliados na carga e antes de cada leitura, fixados pelo chamador
os digests de arquivo protegem o conteúdo do pacote, e só um digest fixado pelo chamador protege o próprio manifest

Se você passar um JavaExecutable vazio, a etapa de schema ainda valida nativamente contra o XSD do pacote, incluindo facets e identity constraints desde a v3.672.0, mas as regras de negócio recuam para o subset embutido e o fullConformance fica false. Construtos de schema que o loader nativo não suporta, como substitution groups, fazem a carga falhar com XSD.Unsupported em vez de serem pulados

Onde veraPDF e Mustang se encaixam

Nenhuma dessas funções olha o contêiner PDF. A conformidade PDF/A-3 sob a ISO 19005-3, o extension schema XMP do Factur-X e o relacionamento de arquivo associado são perguntas separadas, e o veraPDF e o Mustang continuam sendo as ferramentas de referência para elas, como descrito em validar e-faturas com veraPDF e Mustang. O lado de produção, do anexo do XML à declaração do perfil, é coberto em construir faturas híbridas Factur-X e ZUGFeRD no Delphi e extension schemas PDF/A-3 para metadados Factur-X

Uma divisão sensata roda a verificação embutida in-process em toda fatura recebida, onde custa milissegundos: uma fatura UBL de cerca de 1 MB com anexos base64 valida em 15 ms, ou 79 ms com o XSD do UBL 2.1. A execução empacotada do Saxon entra no CI e no caminho de rejeição, e o veraPDF cobre o contêiner. Seja qual for o resultado embutido, ele descreve o subset coberto, não o padrão, que é exatamente o que o campo coverage existe para lembrar

As funções de validação de faturas, o catálogo de regras e as ferramentas de pacote de regras vêm com a Delphi PDF Library para Delphi e C++Builder, junto com as APIs de Factur-X, PDF/A-3 e arquivos embutidos usadas acima