einvoice/test/suite/einvoice_standards-compliance/test.std-04.zugferd-21.ts

455 lines
19 KiB
TypeScript
Raw Normal View History

2025-05-30 04:29:13 +00:00
import { tap, expect } from '@git.zone/tstest/tapbundle';
2025-05-26 04:04:51 +00:00
import * as path from 'path';
import { EInvoice } from '../../../ts/index.js';
2025-05-30 04:29:13 +00:00
import { PerformanceTracker } from '../../helpers/performance.tracker.instance.js';
2025-05-26 04:04:51 +00:00
import { CorpusLoader } from '../../helpers/corpus.loader.js';
2025-05-30 04:29:13 +00:00
tap.test('STD-04: ZUGFeRD 2.1 Compliance - should validate ZUGFeRD 2.1 standard compliance', async () => {
2025-05-26 04:04:51 +00:00
const einvoice = new EInvoice();
const corpusLoader = new CorpusLoader();
2025-05-30 04:29:13 +00:00
const performanceTracker = new PerformanceTracker('STD-04: ZUGFeRD 2.1 Compliance');
2025-05-26 04:04:51 +00:00
// 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;
}
);
2025-05-30 04:29:13 +00:00
expect(profileValidation.length).toEqual(5);
expect(profileValidation.find(p => p.profile === 'EN16931')).toBeTruthy();
2025-05-26 04:04:51 +00:00
// 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,
}
};
}
);
2025-05-30 04:29:13 +00:00
expect(fieldMapping.totalMappings).toBeGreaterThan(15);
expect(fieldMapping.categories.document).toBeGreaterThan(0);
2025-05-26 04:04:51 +00:00
// 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',
};
}
);
2025-05-30 04:29:13 +00:00
expect(namespaceValidation.namespaceCount).toBeGreaterThan(4);
expect(namespaceValidation.rootElement).toEqual('rsm:CrossIndustryInvoice');
2025-05-26 04:04:51 +00:00
// 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)
}))
};
}
);
2025-05-30 04:29:13 +00:00
expect(codeListValidation.codeListCount).toBeGreaterThan(7);
expect(codeListValidation.totalCodes).toBeGreaterThan(50);
2025-05-26 04:04:51 +00:00
// 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'],
};
}
);
2025-05-30 04:29:13 +00:00
expect(calculationRules.ruleCount).toBeGreaterThan(5);
expect(calculationRules.validationTypes).toContain('arithmetic');
2025-05-26 04:04:51 +00:00
// 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'],
};
}
);
2025-05-30 04:29:13 +00:00
expect(businessRules.totalRules).toBeGreaterThan(15);
expect(businessRules.categories.length).toBeGreaterThan(4);
2025-05-26 04:04:51 +00:00
// 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,
};
}
);
2025-05-30 04:29:13 +00:00
expect(attachmentHandling.xmlFilename).toEqual('factur-x.xml');
expect(attachmentHandling.pdfVersion).toEqual('PDF/A-3');
2025-05-26 04:04:51 +00:00
// 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,
})),
};
}
);
2025-05-30 04:29:13 +00:00
expect(profileSpecificValidation.profileCount).toEqual(5);
expect(profileSpecificValidation.profiles.find(p => p.profile === 'EXTENDED')?.forbiddenCount).toEqual(0);
2025-05-26 04:04:51 +00:00
// 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';
2025-05-30 04:29:13 +00:00
const zugferd21Files = await CorpusLoader.loadPattern(zugferd21Pattern, 'ZUGFERD_V2_CORRECT');
2025-05-26 04:04:51 +00:00
results.total = zugferd21Files.length;
// Count by profile
for (const file of zugferd21Files) {
2025-05-30 04:29:13 +00:00
const filename = path.basename(file.path);
2025-05-26 04:04:51 +00:00
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
2025-05-30 04:29:13 +00:00
if (file.path.includes('/correct/')) results.byType.valid++;
else if (file.path.includes('/fail/')) results.byType.invalid++;
2025-05-26 04:04:51 +00:00
}
// Also check for XML files
2025-05-30 04:29:13 +00:00
const xmlFiles = await CorpusLoader.loadPattern('**/*.xml', 'ZUGFERD_V2_CORRECT');
2025-05-26 04:04:51 +00:00
results.byType.xml = xmlFiles.length;
return results;
}
);
2025-05-30 04:29:13 +00:00
expect(corpusValidation.total).toBeGreaterThan(0);
expect(Object.keys(corpusValidation.byProfile).length).toBeGreaterThan(0);
2025-05-26 04:04:51 +00:00
// 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(),
};
}
);
2025-05-30 04:29:13 +00:00
expect(xrechnungCompatibility.compatible).toBeTrue();
expect(xrechnungCompatibility.profile).toEqual('EN16931');
2025-05-26 04:04:51 +00:00
// Generate performance summary
console.log('\n📊 ZUGFeRD 2.1 Compliance Test Summary:');
2025-05-30 04:29:13 +00:00
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
2025-05-26 04:04:51 +00:00
});
2025-05-30 04:29:13 +00:00
// Start the tests
tap.start();
2025-05-26 04:04:51 +00:00
// Export for test runner compatibility
export default tap;