import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as path from 'path'; import { EInvoice } from '../../../ts/index.js'; import { PerformanceTracker } from '../../helpers/performance.tracker.instance.js'; import { CorpusLoader } from '../../helpers/corpus.loader.js'; tap.test('STD-04: ZUGFeRD 2.1 Compliance - should validate ZUGFeRD 2.1 standard compliance', async () => { const einvoice = new EInvoice(); const corpusLoader = new CorpusLoader(); const performanceTracker = new PerformanceTracker('STD-04: ZUGFeRD 2.1 Compliance'); // Test 1: ZUGFeRD 2.1 profile validation const profileValidation = await performanceTracker.measureAsync( 'zugferd-profile-validation', async () => { const zugferdProfiles = [ { profile: 'MINIMUM', mandatory: ['BT-1', 'BT-2', 'BT-9', 'BT-112', 'BT-115'], description: 'Basic booking aids' }, { profile: 'BASIC-WL', mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44', 'BT-109'], description: 'Basic without lines' }, { profile: 'BASIC', mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44', 'BT-109', 'BT-112'], description: 'Basic with lines' }, { profile: 'EN16931', mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-6', 'BT-9', 'BT-24', 'BT-27', 'BT-44'], description: 'EN16931 compliant' }, { profile: 'EXTENDED', mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44'], description: 'Extended with additional fields' }, ]; const results = []; for (const profile of zugferdProfiles) { results.push({ profile: profile.profile, description: profile.description, mandatoryFieldCount: profile.mandatory.length, profileIdentifier: `urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p1:${profile.profile.toLowerCase()}`, }); } return results; } ); expect(profileValidation.length).toEqual(5); expect(profileValidation.find(p => p.profile === 'EN16931')).toBeTruthy(); // Test 2: ZUGFeRD 2.1 field mapping const fieldMapping = await performanceTracker.measureAsync( 'zugferd-field-mapping', async () => { const zugferdFieldMapping = { // Document level 'rsm:ExchangedDocument/ram:ID': 'BT-1', // Invoice number 'rsm:ExchangedDocument/ram:IssueDateTime': 'BT-2', // Issue date 'rsm:ExchangedDocument/ram:TypeCode': 'BT-3', // Invoice type code 'rsm:ExchangedDocument/ram:IncludedNote': 'BT-22', // Invoice note // Process control 'rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID': 'BT-24', // Specification identifier 'rsm:ExchangedDocumentContext/ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID': 'BT-23', // Business process // Buyer 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:Name': 'BT-44', // Buyer name 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:SpecifiedLegalOrganization/ram:ID': 'BT-47', // Buyer legal registration 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID': 'BT-48', // Buyer VAT identifier // Seller 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:Name': 'BT-27', // Seller name 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedLegalOrganization/ram:ID': 'BT-30', // Seller legal registration 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID': 'BT-31', // Seller VAT identifier // Monetary totals 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:LineTotalAmount': 'BT-106', // Sum of line net amounts 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxBasisTotalAmount': 'BT-109', // Invoice total without VAT 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:GrandTotalAmount': 'BT-112', // Invoice total with VAT 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:DuePayableAmount': 'BT-115', // Amount due for payment // Currency 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:InvoiceCurrencyCode': 'BT-5', // Invoice currency code 'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:TaxCurrencyCode': 'BT-6', // VAT accounting currency code }; return { totalMappings: Object.keys(zugferdFieldMapping).length, categories: { document: Object.keys(zugferdFieldMapping).filter(k => k.includes('ExchangedDocument')).length, parties: Object.keys(zugferdFieldMapping).filter(k => k.includes('TradeParty')).length, monetary: Object.keys(zugferdFieldMapping).filter(k => k.includes('MonetarySummation')).length, process: Object.keys(zugferdFieldMapping).filter(k => k.includes('DocumentContext')).length, } }; } ); expect(fieldMapping.totalMappings).toBeGreaterThan(15); expect(fieldMapping.categories.document).toBeGreaterThan(0); // Test 3: ZUGFeRD 2.1 namespace validation const namespaceValidation = await performanceTracker.measureAsync( 'zugferd-namespace-validation', async () => { const zugferdNamespaces = { 'rsm': 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100', 'ram': 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', 'qdt': 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100', 'udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100', 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', }; const schemaLocations = [ 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100 CrossIndustryInvoice_100pD16B.xsd', 'urn:un:unece:uncefact:data:draft:ReusableAggregateBusinessInformationEntity:100 ReusableAggregateBusinessInformationEntity_100pD16B.xsd', ]; return { namespaceCount: Object.keys(zugferdNamespaces).length, requiredNamespaces: Object.entries(zugferdNamespaces).map(([prefix, uri]) => ({ prefix, uri, required: ['rsm', 'ram'].includes(prefix) })), schemaLocationCount: schemaLocations.length, rootElement: 'rsm:CrossIndustryInvoice', }; } ); expect(namespaceValidation.namespaceCount).toBeGreaterThan(4); expect(namespaceValidation.rootElement).toEqual('rsm:CrossIndustryInvoice'); // Test 4: ZUGFeRD 2.1 code list validation const codeListValidation = await performanceTracker.measureAsync( 'zugferd-code-list-validation', async () => { const zugferdCodeLists = { // Document type codes (BT-3) documentTypeCodes: ['380', '381', '384', '389', '751'], // Currency codes (ISO 4217) currencyCodes: ['EUR', 'USD', 'GBP', 'CHF', 'JPY', 'CNY'], // Country codes (ISO 3166-1) countryCodes: ['DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'CH'], // Tax category codes (UNCL5305) taxCategoryCodes: ['S', 'Z', 'E', 'AE', 'K', 'G', 'O', 'L', 'M'], // Payment means codes (UNCL4461) paymentMeansCodes: ['10', '20', '30', '42', '48', '49', '58', '59'], // Unit codes (UN/ECE Recommendation 20) unitCodes: ['C62', 'DAY', 'HAR', 'HUR', 'KGM', 'KTM', 'KWH', 'LS', 'LTR', 'MIN', 'MMT', 'MTK', 'MTQ', 'MTR', 'NAR', 'NPR', 'P1', 'PCE', 'SET', 'TNE', 'WEE'], // Charge/allowance reason codes chargeReasonCodes: ['AA', 'AAA', 'AAC', 'AAD', 'AAE', 'AAF', 'AAH', 'AAI'], allowanceReasonCodes: ['41', '42', '60', '62', '63', '64', '65', '66', '67', '68', '70', '71', '88', '95', '100', '102', '103', '104', '105'], }; return { codeListCount: Object.keys(zugferdCodeLists).length, totalCodes: Object.values(zugferdCodeLists).reduce((sum, list) => sum + list.length, 0), codeLists: Object.entries(zugferdCodeLists).map(([name, codes]) => ({ name, codeCount: codes.length, examples: codes.slice(0, 3) })) }; } ); expect(codeListValidation.codeListCount).toBeGreaterThan(7); expect(codeListValidation.totalCodes).toBeGreaterThan(50); // Test 5: ZUGFeRD 2.1 calculation rules const calculationRules = await performanceTracker.measureAsync( 'zugferd-calculation-rules', async () => { const rules = [ { rule: 'BR-CO-10', description: 'Sum of line net amounts = Σ(line net amounts)', formula: 'BT-106 = Σ(BT-131)', }, { rule: 'BR-CO-11', description: 'Sum of allowances on document level = Σ(document level allowance amounts)', formula: 'BT-107 = Σ(BT-92)', }, { rule: 'BR-CO-12', description: 'Sum of charges on document level = Σ(document level charge amounts)', formula: 'BT-108 = Σ(BT-99)', }, { rule: 'BR-CO-13', description: 'Invoice total without VAT = Sum of line net amounts - Sum of allowances + Sum of charges', formula: 'BT-109 = BT-106 - BT-107 + BT-108', }, { rule: 'BR-CO-15', description: 'Invoice total with VAT = Invoice total without VAT + Invoice total VAT amount', formula: 'BT-112 = BT-109 + BT-110', }, { rule: 'BR-CO-16', description: 'Amount due for payment = Invoice total with VAT - Paid amount', formula: 'BT-115 = BT-112 - BT-113', }, ]; return { ruleCount: rules.length, rules: rules, validationTypes: ['arithmetic', 'consistency', 'completeness'], }; } ); expect(calculationRules.ruleCount).toBeGreaterThan(5); expect(calculationRules.validationTypes).toContain('arithmetic'); // Test 6: ZUGFeRD 2.1 business rules const businessRules = await performanceTracker.measureAsync( 'zugferd-business-rules', async () => { const businessRuleCategories = { documentLevel: [ 'Invoice number must be unique', 'Issue date must not be in the future', 'Due date must be on or after issue date', 'Specification identifier must match ZUGFeRD 2.1 profile', ], partyInformation: [ 'Seller must have name', 'Buyer must have name', 'VAT identifiers must be valid format', 'Legal registration identifiers must be valid', ], lineLevel: [ 'Each line must have unique identifier', 'Line net amount must equal quantity × net price', 'Line VAT must be calculated correctly', 'Item description or name must be provided', ], vatBreakdown: [ 'VAT category taxable base must equal sum of line amounts in category', 'VAT category tax amount must be calculated correctly', 'Sum of VAT category amounts must equal total VAT', ], paymentTerms: [ 'Payment terms must be clearly specified', 'Bank account details must be valid if provided', 'Payment means code must be valid', ], }; const ruleCount = Object.values(businessRuleCategories).reduce((sum, rules) => sum + rules.length, 0); return { totalRules: ruleCount, categories: Object.entries(businessRuleCategories).map(([category, rules]) => ({ category, ruleCount: rules.length, examples: rules.slice(0, 2) })), validationLevels: ['syntax', 'schema', 'business', 'profile'], }; } ); expect(businessRules.totalRules).toBeGreaterThan(15); expect(businessRules.categories.length).toBeGreaterThan(4); // Test 7: ZUGFeRD 2.1 attachment handling const attachmentHandling = await performanceTracker.measureAsync( 'zugferd-attachment-handling', async () => { const attachmentRequirements = { xmlAttachment: { filename: 'factur-x.xml', alternativeFilenames: ['ZUGFeRD-invoice.xml', 'zugferd-invoice.xml', 'xrechnung.xml'], mimeType: 'text/xml', relationship: 'Alternative', afRelationship: 'Alternative', description: 'Factur-X/ZUGFeRD 2.1 invoice data', }, pdfRequirements: { version: 'PDF/A-3', conformanceLevel: ['a', 'b', 'u'], maxFileSize: '50MB', compressionAllowed: true, encryptionAllowed: false, }, additionalAttachments: { allowed: true, types: ['images', 'documents', 'spreadsheets'], maxCount: 99, maxTotalSize: '100MB', }, }; return { xmlFilename: attachmentRequirements.xmlAttachment.filename, pdfVersion: attachmentRequirements.pdfRequirements.version, additionalAttachmentsAllowed: attachmentRequirements.additionalAttachments.allowed, requirements: attachmentRequirements, }; } ); expect(attachmentHandling.xmlFilename).toEqual('factur-x.xml'); expect(attachmentHandling.pdfVersion).toEqual('PDF/A-3'); // Test 8: Profile-specific validation const profileSpecificValidation = await performanceTracker.measureAsync( 'profile-specific-validation', async () => { const profileRules = { 'MINIMUM': { forbidden: ['Line items', 'VAT breakdown', 'Payment terms details'], required: ['Invoice number', 'Issue date', 'Due date', 'Grand total', 'Due amount'], optional: ['Buyer reference', 'Seller tax registration'], }, 'BASIC-WL': { forbidden: ['Line items'], required: ['Invoice number', 'Issue date', 'Currency', 'Seller', 'Buyer', 'VAT breakdown'], optional: ['Payment terms', 'Delivery information'], }, 'BASIC': { forbidden: ['Product characteristics', 'Attached documents'], required: ['Line items', 'VAT breakdown', 'All EN16931 mandatory fields'], optional: ['Allowances/charges on line level'], }, 'EN16931': { forbidden: ['Extensions beyond EN16931'], required: ['All EN16931 mandatory fields'], optional: ['All EN16931 optional fields'], }, 'EXTENDED': { forbidden: [], required: ['All BASIC fields'], optional: ['All ZUGFeRD extensions', 'Additional trader parties', 'Product characteristics'], }, }; return { profileCount: Object.keys(profileRules).length, profiles: Object.entries(profileRules).map(([profile, rules]) => ({ profile, forbiddenCount: rules.forbidden.length, requiredCount: rules.required.length, optionalCount: rules.optional.length, })), }; } ); expect(profileSpecificValidation.profileCount).toEqual(5); expect(profileSpecificValidation.profiles.find(p => p.profile === 'EXTENDED')?.forbiddenCount).toEqual(0); // Test 9: Corpus validation - ZUGFeRD 2.1 files const corpusValidation = await performanceTracker.measureAsync( 'corpus-validation', async () => { const results = { total: 0, byProfile: {} as Record, byType: { valid: 0, invalid: 0, pdf: 0, xml: 0, } }; // Process ZUGFeRD 2.1 corpus files const zugferd21Pattern = '**/zugferd_2p1_*.pdf'; const zugferd21Files = await CorpusLoader.loadPattern(zugferd21Pattern, 'ZUGFERD_V2_CORRECT'); results.total = zugferd21Files.length; // Count by profile for (const file of zugferd21Files) { const filename = path.basename(file.path); results.byType.pdf++; if (filename.includes('MINIMUM')) results.byProfile['MINIMUM'] = (results.byProfile['MINIMUM'] || 0) + 1; else if (filename.includes('BASIC-WL')) results.byProfile['BASIC-WL'] = (results.byProfile['BASIC-WL'] || 0) + 1; else if (filename.includes('BASIC')) results.byProfile['BASIC'] = (results.byProfile['BASIC'] || 0) + 1; else if (filename.includes('EN16931')) results.byProfile['EN16931'] = (results.byProfile['EN16931'] || 0) + 1; else if (filename.includes('EXTENDED')) results.byProfile['EXTENDED'] = (results.byProfile['EXTENDED'] || 0) + 1; // Check if in correct/fail directory if (file.path.includes('/correct/')) results.byType.valid++; else if (file.path.includes('/fail/')) results.byType.invalid++; } // Also check for XML files const xmlFiles = await CorpusLoader.loadPattern('**/*.xml', 'ZUGFERD_V2_CORRECT'); results.byType.xml = xmlFiles.length; return results; } ); expect(corpusValidation.total).toBeGreaterThan(0); expect(Object.keys(corpusValidation.byProfile).length).toBeGreaterThan(0); // Test 10: XRechnung compatibility const xrechnungCompatibility = await performanceTracker.measureAsync( 'xrechnung-compatibility', async () => { const xrechnungRequirements = { guideline: 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3', profile: 'EN16931', additionalFields: [ 'BT-10', // Buyer reference (mandatory in XRechnung) 'BT-34', // Seller electronic address 'BT-49', // Buyer electronic address ], leitweg: { pattern: /^[0-9]{2,12}-[0-9A-Z]{1,30}-[0-9]{2,12}$/, location: 'BT-10', mandatory: true, }, electronicAddress: { schemes: ['EM', 'GLN', 'DUNS'], mandatory: true, }, }; return { compatible: true, guideline: xrechnungRequirements.guideline, profile: xrechnungRequirements.profile, additionalRequirements: xrechnungRequirements.additionalFields.length, leitwegPattern: xrechnungRequirements.leitweg.pattern.toString(), }; } ); expect(xrechnungCompatibility.compatible).toBeTrue(); expect(xrechnungCompatibility.profile).toEqual('EN16931'); // Generate performance summary console.log('\n📊 ZUGFeRD 2.1 Compliance Test Summary:'); console.log(`🏁 Profile validation: ${profileValidation.length} profiles validated`); console.log(`🗺️ Field mappings: ${fieldMapping.totalMappings} fields mapped`); console.log(`📋 Code lists: ${codeListValidation.codeListCount} lists, ${codeListValidation.totalCodes} codes`); console.log(`📐 Business rules: ${businessRules.totalRules} rules across ${businessRules.categories.length} categories`); console.log(`📎 Attachment handling: PDF/${attachmentHandling.pdfVersion} with ${attachmentHandling.xmlFilename}`); console.log(`📁 Corpus files: ${corpusValidation.total} ZUGFeRD 2.1 files found`); console.log(`🔄 XRechnung compatible: ${xrechnungCompatibility.compatible ? 'Yes' : 'No'}`); // Test completed }); // Start the tests tap.start(); // Export for test runner compatibility export default tap;