einvoice/test/suite/einvoice_standards-compliance/test.std-04.zugferd-21.ts
2025-05-26 04:04:51 +00:00

461 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { tap } from '@git.zone/tstest/tapbundle';
import * as path from 'path';
import { EInvoice } from '../../../ts/index.js';
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
import { CorpusLoader } from '../../helpers/corpus.loader.js';
tap.test('STD-04: ZUGFeRD 2.1 Compliance - should validate ZUGFeRD 2.1 standard compliance', async (t) => {
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;
}
);
t.ok(profileValidation.result.length === 5, 'Should validate all ZUGFeRD 2.1 profiles');
t.ok(profileValidation.result.find(p => p.profile === 'EN16931'), 'Should include EN16931 profile');
// 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,
}
};
}
);
t.ok(fieldMapping.result.totalMappings > 15, 'Should have comprehensive field mappings');
t.ok(fieldMapping.result.categories.document > 0, 'Should map document level fields');
// 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',
};
}
);
t.ok(namespaceValidation.result.namespaceCount >= 5, 'Should define required namespaces');
t.ok(namespaceValidation.result.rootElement === 'rsm:CrossIndustryInvoice', 'Should use correct root element');
// 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)
}))
};
}
);
t.ok(codeListValidation.result.codeListCount >= 8, 'Should validate multiple code lists');
t.ok(codeListValidation.result.totalCodes > 50, 'Should have comprehensive code coverage');
// 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'],
};
}
);
t.ok(calculationRules.result.ruleCount >= 6, 'Should include calculation rules');
t.ok(calculationRules.result.validationTypes.includes('arithmetic'), 'Should validate arithmetic calculations');
// 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'],
};
}
);
t.ok(businessRules.result.totalRules > 15, 'Should have comprehensive business rules');
t.ok(businessRules.result.categories.length >= 5, 'Should cover all major categories');
// 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,
};
}
);
t.ok(attachmentHandling.result.xmlFilename === 'factur-x.xml', 'Should use standard XML filename');
t.ok(attachmentHandling.result.pdfVersion === 'PDF/A-3', 'Should require 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,
})),
};
}
);
t.ok(profileSpecificValidation.result.profileCount === 5, 'Should validate all profiles');
t.ok(profileSpecificValidation.result.profiles.find(p => p.profile === 'EXTENDED')?.forbiddenCount === 0, 'EXTENDED profile should allow all fields');
// Test 9: Corpus validation - ZUGFeRD 2.1 files
const corpusValidation = await performanceTracker.measureAsync(
'corpus-validation',
async () => {
const results = {
total: 0,
byProfile: {} as Record<string, number>,
byType: {
valid: 0,
invalid: 0,
pdf: 0,
xml: 0,
}
};
// Process ZUGFeRD 2.1 corpus files
const zugferd21Pattern = '**/zugferd_2p1_*.pdf';
const zugferd21Files = await corpusLoader.findFiles('ZUGFeRDv2', zugferd21Pattern);
results.total = zugferd21Files.length;
// Count by profile
for (const file of zugferd21Files) {
const filename = path.basename(file);
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.includes('/correct/')) results.byType.valid++;
else if (file.includes('/fail/')) results.byType.invalid++;
}
// Also check for XML files
const xmlFiles = await corpusLoader.findFiles('ZUGFeRDv2', '**/*.xml');
results.byType.xml = xmlFiles.length;
return results;
}
);
t.ok(corpusValidation.result.total > 0, 'Should find ZUGFeRD 2.1 corpus files');
t.ok(Object.keys(corpusValidation.result.byProfile).length > 0, 'Should categorize files by profile');
// 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(),
};
}
);
t.ok(xrechnungCompatibility.result.compatible, 'Should be XRechnung compatible');
t.ok(xrechnungCompatibility.result.profile === 'EN16931', 'Should use EN16931 profile for XRechnung');
// Generate performance summary
const summary = performanceTracker.getSummary();
console.log('\n📊 ZUGFeRD 2.1 Compliance Test Summary:');
console.log(`✅ Total operations: ${summary.totalOperations}`);
console.log(`⏱️ Total duration: ${summary.totalDuration}ms`);
console.log(`🏁 Profile validation: ${profileValidation.result.length} profiles validated`);
console.log(`🗺️ Field mappings: ${fieldMapping.result.totalMappings} fields mapped`);
console.log(`📋 Code lists: ${codeListValidation.result.codeListCount} lists, ${codeListValidation.result.totalCodes} codes`);
console.log(`📐 Business rules: ${businessRules.result.totalRules} rules across ${businessRules.result.categories.length} categories`);
console.log(`📎 Attachment handling: PDF/${attachmentHandling.result.pdfVersion} with ${attachmentHandling.result.xmlFilename}`);
console.log(`📁 Corpus files: ${corpusValidation.result.total} ZUGFeRD 2.1 files found`);
console.log(`🔄 XRechnung compatible: ${xrechnungCompatibility.result.compatible ? 'Yes' : 'No'}`);
console.log('\n🔍 Performance breakdown:');
summary.operations.forEach(op => {
console.log(` - ${op.name}: ${op.duration}ms`);
});
t.end();
});
// Export for test runner compatibility
export default tap;