Teknisk artikel

Validering av e-fakturor: veraPDF och Mustang i Delphi

En Factur-X- eller ZUGFeRD-faktura är två dokument som bär ett enda filnamn. Det yttre dokumentet är en PDF/A-3-behållare (container) som en arkivläsare måste acceptera under de kommande tio åren. Det inre dokumentet är en XML-faktura som en köpares bokföringssystem måste tolka (parse) gentemot EN 16931. Det misstag som skickar trasiga (broken) fakturor i produktion är att tro att om man får det första rätt så får man det andra på köpet. Så är inte fallet. En fil kan vara en felfri PDF/A-3 och ändå bära på XML som ingen skattemyndighet kommer att acceptera, och den kan bära på en textboks-enlig (textbook) EN 16931-XML inuti en behållare som fallerar vid arkivvalidering (archival validation). De två lagren valideras av två olika verktyg som inte vet någonting om varandra, och en riktig pipeline måste tillfredsställa (satisfy) båda

Två validatorer, två olika frågor

veraPDF är referensimplementationen (reference implementation) för PDF/A. Peka den mot en faktura och den svarar på en fråga: är detta en PDF/A-3-fil som uppfyller kraven (conformant). Den kontrollerar de saker ISO 19005-3 bryr sig om. Är alla typsnitt inbäddade (embedded). Finns det ett OutputIntent. Deklarerar XMP-metadatan rätt del och efterlevnadsnivå (conformance level). För en e-faktura kontrollerar den också rörsystemet för associerade filer (associated-file plumbing) som PDF/A-3 kräver, eftersom XML:en åker med som en inbäddad fil med ett /AFRelationship och en post i dokumentkatalogens (document catalog's) /AF-array. veraPDF säger ingenting om ifall fakturans totalbelopp summerar rätt, eftersom det inte ligger inom dess ansvarsområde (remit)

Mustang är en öppen källkods-validator (open-source validator) från Mustangproject. Den ställer den ortogonala frågan: är den inbäddade XML:en en giltig faktura. Den kör XML:en mot schemat (schema) för den deklarerade profilen och tillämpar sedan EN 16931:s affärsregler och de landsspecifika regelverken (rule sets) som ligger ovanpå, däribland XRechnungs CIUS. Den kontrollerar att ett säljar-momsregistreringsnummer (seller VAT identifier) finns när totalerna kräver ett, att rabatt- och avgiftsbelopp (allowance and charge amounts) stäms av (reconcile) mot dokumentets totalbelopp, och att profil-URN:en i XML:en matchar det som filen påstår sig vara. Mustang bryr sig inte om ifall den omgivande PDF:en bäddar in (embeds) sina typsnitt, eftersom det är veraPDF:s jobb

Ingetdera av verktygen är en övermängd (superset) av det andra. veraPDF godkänner (passes) en strukturellt perfekt behållare som omger en nonsens-XML. Mustang godkänner perfekt XML inlindad (wrapped) i en behållare som saknar ett OutputIntent. Var och en fångar exakt den klass av defekter som den andra är blind för, vilket är hela anledningen till varför en seriös valideringsrigg (validation harness) kör båda och behandlar en fil som skeppbar (shippable) först när båda är överens

Valideringsmatrisen

För att bevisa att biblioteket producerar filer som överlever båda grindarna (gates), bygger riggen en matris. Sex fakturaprofiler täcker den bredd (range) en europeisk pipeline möter i praktiken: Factur-X EN 16931, Factur-X BASIC, varianten Factur-X EXTENDED France B2B, XRechnung 3.0, ZUGFeRD 1.0 COMFORT, och ZUGFeRD 2.0 BASIC. Varje profil genereras mot två PDF/A-underefterlevnadsnivåer (sub-conformance levels), 3b och 3u, eftersom nivå B- och nivå U-kraven skiljer sig åt gällande Unicode-mappning och en fil som klarar det ena kan underkännas (fail) på det andra. Sex profiler gånger två nivåer blir tolv filer, var och en av dem bygd huvudlöst (headless) av samma kodsökväg (code path) som GUI-exemplet skeppar med, så artefakterna under test handjusteras inte (not hand-tuned) för testet

Generatorn skriver ut alla tolv och ett skript matar varje in till båda validatorerna. På den första hela körningen godkände veraPDF alla tolv. Behållarens rörsystem (container plumbing) var korrekt över hela linjen: associerade filer registrerade, XMP-efterlevnad deklarerad, utdataintentioner (output intents) på plats. Mustang godkände åtta. Fyra fakturor var strukturellt giltiga PDF/A-3-filer som bar på XML som affärsregelsvalidatorn förkastade (rejected), vilket är exakt den splittring (split) tvåverktygsstrategin existerar för att lyfta fram (surface). Hade riggen förlitat sig enbart på veraPDF skulle de fyra ha verkat färdiga

De två fixarna som slöt klyftan

De fyra Mustang-misslyckandena kom från två distinkta orsaker, och fixen för var och en är en detalj värd att veta innan du genererar dessa profiler själv

Den första var Factur-X EXTENDED France B2B-profilen. Originalgeneratorn skickade en intern etikett (internal label) som efterlevnadsnivå (conformance level) och en intern URN som riktlinje (guideline), och Mustang förkastade filen med ett fel om ogiltigt efterlevnadsvärde (invalid-conformance-value) följt av ett fel om icke stödd profiltyp (unsupported-profile-type). Anledningen är att XMP-fältet fx:ConformanceLevel inte är en fritextfack (free-text slot) för din egen profilnamngivning. Factur-X definierar exakt fem standardvärden för det: MINIMUM, BASIC WL, BASIC, EN 16931, och EXTENDED. En frankrike-specifik B2B-faktura är fortfarande ett EXTENDED-profil-dokument så långt som XMP-metadatan berörs. Det franska draget (character) av fakturan uttrycks inte genom att man uppfinner ett sjätte efterlevnadsvärde. Det uttrycks genom landskoden, FR, och genom riktlinjeidentifieraren (guideline identifier) inuti XML:en, vilken måste bära prefixet urn:cen.eu:en16931:2017#conformant# som markerar en CIUS som uppfyller EN 16931. Genom att skicka (passing) standardvärdet EXTENDED med FR som landskod och den korrekta riktlinje-URN:en gjordes filen konform (conformant)

I bibliotekets API är det ett anrop till AddFacturXAssociatedFileFromString med efterlevnad, land och riktlinje i linje (aligned). Efterlevnadsnivå-argumentet (The conformance level argument) bär standard-token, landskodsargumentet bär FR, och riktlinje-URN:en lever i de XML-bytes du skickar in

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;

Den andra orsaken var ZUGFeRD 1.0 COMFORT-profilen, och det hade ingenting med metadata att göra. ZUGFeRD 1.0 valideras mot :1p0 XSD:n, som är strängare gällande kardinalitet (cardinality) än vad de sammanfattande texterna (prose summaries) antyder (suggest). XSD:n kräver att huvudavvecklingssummeringen (header settlement summation), ram:SpecifiedTradeSettlementMonetarySummation, innehåller ram:ChargeTotalAmount och ram:AllowanceTotalAmount vardera exakt en gång. Den genererade XML:en utelämnade båda, så Mustang rapporterade att elementen måste förekomma exakt en gång. Dessa är inte valfria (optional) när schemat säger minOccurs är ett (one). Genom att skriva ut (Emitting) båda i XSD-sekvensordning (XSD sequence order), omedelbart efter ram:LineTotalAmount, med ett värde av 0.00 när det inte finns några avgifter eller rabatter (charges or allowances), tillfredsställdes (satisfied) schemat. En nolla är ett närvarande (present) element; ett frånvarande element är ett brott mot schemat (schema violation). Med dessa två fixar på plats gick matrisen till tolv av tolv på Mustang, medan den förblev tolv av tolv på veraPDF

XRechnung-fälten som vänder ogiltigt till giltigt

XRechnung förtjänar sin egen anmärkning (note) eftersom dess tyska CIUS lägger till affärsregler som saknas från basen EN 16931, och de fallerar på sätt som gör att det vid första anblicken (at a glance) inte ser ut som att något är fel med dokumentet. Två av dem rör elektroniska adresser. BT-34 är säljarens elektroniska adress och BT-49 är köparens elektroniska adress, routing-ändpunkterna (routing endpoints) som en tysk portal inom offentlig sektor använder för att leverera och bekräfta fakturan. Basmodellen EN 16931 behandlar dem som valfria. Det gör inte XRechnung. Utelämna endera (Omit either) och fakturan är välformad (well-formed), giltig enligt schemat, och förkastas (rejected)

Den tredje är regeln BR-DE-6, vilken kräver att säljarens kontakttelefonnummer finns närvarande (to be present). Det är den sortens fält som en utvecklare tappar bort (drops) för att det känns som presentation snarare än data, och dess frånvaro resulterar i ett valideringsfel som pekar på säljarkontaktgruppen (seller contact group) i stället för på något uppenbart saknat. Att tillhandahålla (Supplying) BT-34, BT-49 och säljarens telefonnummer är vad som flyttar en XRechnung-fil från ogiltig till giltig under Mustang, och ingenting av det ändrar något veraPDF ser, eftersom alla tre bor i XML:en

Att koppla bibliotekets utmatning till en validator

Arkitekturpoängen (The architectural point) bakom riggen (harness) generaliseras (generalizes) till vilket affärssystem (business system) som helst. PDF-biblioteket skriver en efterlevande (conformant) behållare och bäddar in XML:en. Det varken försöker, eller bör försöka, vara affärsregelsauktoriteten för EN 16931. ValidateFacturXInvoice i biblioteket kontrollerar behållarens konsistens, att katalogen /AF-array, de inbäddade filernas namnträd (embedded-files name tree), XMP DocumentFileName, profilen, riktlinjen och /AFRelationship alla är överens, men den validerar inte skattekoder (tax codes) eller stämmer av belopp (reconcile amounts). Rätt arbetsdelning (division of labor) är att affärssystemet drar ut (extract) XML:en och överlämnar den till en dedikerad fakturavalidator, exakt så som riggen lämnar över den till Mustang

Att läsa filen tillbaka talar om för dig vad som faktiskt skrevs. DetectFacturXInvoice rapporterar ifall en faktura kändes igen, och GetFacturXInvoiceInfo läser metadatafälten per tagg: tagg 1 är det inbäddade filnamnet, tagg 2 är XMP DocumentFileName, tagg 5 är efterlevnadsnivån (conformance level), tagg 6 är riktlinjeidentifieraren (guideline identifier) och tagg 7 är /AFRelationship. Att bekräfta att efterlevnadsnivån som du läser tillbaka är standard-token och inte en intern etikett (internal label) är det billigaste sättet att fånga EXTENDED-misstaget innan en fil lämnar ditt bygge (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 returnerar de råa XML-byten som en AnsiString, redo att skrivas till en fil eller strömmas (stream) in i en validatorprocess. I testriggen är det målet Mustang, anropad (invoked) via dess kommandorads-jar (command-line jar), med veraPDF körd i samma passering (pass) över samma fil. Inkopplingen (wiring) är liten: en konsolgenerator (console generator), EInvoiceValidation.dpr, skriver de tolv filerna via (using) den delade fakturamodellen från exemplet, och ett skript, run-validation.ps1, driver båda validatorerna (drives both validators) över utmatningskatalogen (output directory) och skriver ut en godkänt och underkänt-tabell (pass and fail table). Samma tvåstegsform, generera med biblioteket och verifiera med externa validatorer, är vad ett kontinuerligt integrationsjobb (continuous-integration job) borde köra vid varje förändring i fakturagenerering, för det enda sättet att veta att en fil tillfredsställer båda lagren är att fråga båda verktygen

Om din pipeline också måste intyga (certify) behållaren innan signering, är preflight-sidan av det här arbetet täckt i vår genomgång av PDF/A och PDF/UA preflight i Delphi, och det bredare intyga-sedan-signera-flödet (certify-then-sign flow) beskrivs i arbetsbänken för efterlevnad och signering. Båda bygger på samma genereringssökväg (generation path) som skeppas som en del av Delphi PDF Library för Delphi och C++Builder, tillsammans med API:erna för PDF/A, associerade filer (associated-file) och metadata som används här