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