BREAKING CHANGE(core): Rebrand XInvoice to EInvoice: update package name, class names, imports, and documentation

This commit is contained in:
2025-05-24 16:33:58 +00:00
parent 805adc6d5c
commit a93ea090ce
27 changed files with 3172 additions and 295 deletions

370
test/test-utils.ts Normal file
View File

@ -0,0 +1,370 @@
import * as path from 'path';
import { promises as fs } from 'fs';
import { EInvoice } from '../ts/einvoice.js';
import type { TInvoice } from '../ts/interfaces/common.js';
import { InvoiceFormat } from '../ts/interfaces/common.js';
import { business, finance } from '../ts/plugins.js';
/**
* Test utilities for EInvoice testing
*/
/**
* Test file categories based on the corpus
*/
export const TestFileCategories = {
CII_XMLRECHNUNG: 'test/assets/corpus/XML-Rechnung/CII',
UBL_XMLRECHNUNG: 'test/assets/corpus/XML-Rechnung/UBL',
ZUGFERD_V1_CORRECT: 'test/assets/corpus/ZUGFeRDv1/correct',
ZUGFERD_V1_FAIL: 'test/assets/corpus/ZUGFeRDv1/fail',
ZUGFERD_V2_CORRECT: 'test/assets/corpus/ZUGFeRDv2/correct',
ZUGFERD_V2_FAIL: 'test/assets/corpus/ZUGFeRDv2/fail',
PEPPOL: 'test/assets/corpus/PEPPOL/Valid/Qvalia',
FATTURAPA: 'test/assets/corpus/fatturaPA',
EN16931_UBL_INVOICE: 'test/assets/eInvoicing-EN16931/test/Invoice-unit-UBL',
EN16931_UBL_CREDITNOTE: 'test/assets/eInvoicing-EN16931/test/CreditNote-unit-UBL',
EN16931_EXAMPLES_CII: 'test/assets/eInvoicing-EN16931/cii/examples',
EN16931_EXAMPLES_UBL: 'test/assets/eInvoicing-EN16931/ubl/examples',
EN16931_EXAMPLES_EDIFACT: 'test/assets/eInvoicing-EN16931/edifact/examples'
} as const;
/**
* Test data factory for creating test invoices
*/
export class TestInvoiceFactory {
/**
* Creates a minimal valid test invoice
*/
static createMinimalInvoice(): Partial<TInvoice> {
return {
id: 'TEST-' + Date.now(),
invoiceId: 'INV-TEST-001',
invoiceType: 'debitnote',
type: 'invoice',
date: Date.now(),
status: 'draft',
subject: 'Test Invoice',
from: {
name: 'Test Seller Company',
type: 'company',
description: 'Test seller',
address: {
streetName: 'Test Street',
houseNumber: '1',
city: 'Test City',
country: 'Germany',
postalCode: '12345'
},
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Test Registry'
}
},
to: {
name: 'Test Buyer Company',
type: 'company',
description: 'Test buyer',
address: {
streetName: 'Buyer Street',
houseNumber: '2',
city: 'Buyer City',
country: 'France',
postalCode: '75001'
},
status: 'active',
foundedDate: { year: 2019, month: 6, day: 15 },
registrationDetails: {
vatId: 'FR987654321',
registrationId: 'RCS 98765',
registrationName: 'French Registry'
}
},
items: [{
position: 1,
name: 'Test Product',
articleNumber: 'TEST-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}],
currency: 'EUR',
language: 'en',
objectActions: [],
versionInfo: {
type: 'draft',
version: '1.0.0'
}
};
}
/**
* Creates a complex test invoice with multiple items and features
*/
static createComplexInvoice(): Partial<TInvoice> {
const baseInvoice = this.createMinimalInvoice();
return {
...baseInvoice,
items: [
{
position: 1,
name: 'Professional Service',
articleNumber: 'SERV-001',
unitType: 'HUR',
unitQuantity: 8,
unitNetPrice: 150,
vatPercentage: 19,
// description: 'Consulting services'
},
{
position: 2,
name: 'Software License',
articleNumber: 'SOFT-001',
unitType: 'EA',
unitQuantity: 5,
unitNetPrice: 200,
vatPercentage: 19,
// description: 'Annual software license'
},
{
position: 3,
name: 'Training',
articleNumber: 'TRAIN-001',
unitType: 'DAY',
unitQuantity: 2,
unitNetPrice: 800,
vatPercentage: 19,
// description: 'On-site training'
}
],
paymentOptions: {
description: 'Payment due within 30 days',
sepaConnection: {
iban: 'DE89370400440532013000',
bic: 'COBADEFFXXX'
},
payPal: { email: 'test@example.com' }
},
notes: [
'This is a test invoice for validation purposes',
'All amounts are in EUR'
],
periodOfPerformance: {
from: Date.now() - 30 * 24 * 60 * 60 * 1000, // 30 days ago
to: Date.now()
},
deliveryDate: Date.now(),
buyerReference: 'PO-2024-001',
dueInDays: 30,
reverseCharge: false
};
}
}
/**
* Test file helpers
*/
export class TestFileHelpers {
/**
* Gets all test files from a directory
*/
static async getTestFiles(directory: string, pattern: string = '*'): Promise<string[]> {
const basePath = path.join(process.cwd(), directory);
const files: string[] = [];
try {
const entries = await fs.readdir(basePath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile()) {
const fileName = entry.name;
if (pattern === '*' || fileName.match(pattern.replace('*', '.*'))) {
files.push(path.join(directory, fileName));
}
}
}
} catch (error) {
console.error(`Error reading directory ${basePath}:`, error);
}
return files;
}
/**
* Loads a test file
*/
static async loadTestFile(filePath: string): Promise<Buffer> {
const fullPath = path.join(process.cwd(), filePath);
return fs.readFile(fullPath);
}
/**
* Gets corpus statistics
*/
static async getCorpusStats(): Promise<{
totalFiles: number;
byFormat: Record<string, number>;
byCategory: Record<string, number>;
}> {
const stats = {
totalFiles: 0,
byFormat: {} as Record<string, number>,
byCategory: {} as Record<string, number>
};
for (const [category, path] of Object.entries(TestFileCategories)) {
const files = await this.getTestFiles(path, '*.xml');
const pdfFiles = await this.getTestFiles(path, '*.pdf');
const totalCategoryFiles = files.length + pdfFiles.length;
stats.totalFiles += totalCategoryFiles;
stats.byCategory[category] = totalCategoryFiles;
}
return stats;
}
}
/**
* Test assertions for invoice validation
*/
export class InvoiceAssertions {
/**
* Asserts that an invoice has all required fields
*/
static assertRequiredFields(invoice: EInvoice): void {
const requiredFields = ['id', 'invoiceId', 'from', 'to', 'items', 'date'];
for (const field of requiredFields) {
if (!invoice[field as keyof EInvoice]) {
throw new Error(`Required field '${field}' is missing`);
}
}
// Check nested required fields
if (!invoice.from.name || !invoice.from.address) {
throw new Error('Seller information incomplete');
}
if (!invoice.to.name || !invoice.to.address) {
throw new Error('Buyer information incomplete');
}
if (!invoice.items || invoice.items.length === 0) {
throw new Error('Invoice must have at least one item');
}
}
/**
* Asserts that format detection works correctly
*/
static assertFormatDetection(
detectedFormat: InvoiceFormat,
expectedFormat: InvoiceFormat,
filePath: string
): void {
if (detectedFormat !== expectedFormat) {
throw new Error(
`Format detection failed for ${filePath}: expected ${expectedFormat}, got ${detectedFormat}`
);
}
}
/**
* Asserts validation results
*/
static assertValidationResult(
result: { valid: boolean; errors: any[] },
expectedValid: boolean,
filePath: string
): void {
if (result.valid !== expectedValid) {
const errorMessages = result.errors.map(e => e.message).join(', ');
throw new Error(
`Validation result mismatch for ${filePath}: expected ${expectedValid}, got ${result.valid}. Errors: ${errorMessages}`
);
}
}
}
/**
* Performance testing utilities
*/
export class PerformanceUtils {
private static measurements = new Map<string, number[]>();
/**
* Measures execution time of an async function
*/
static async measure<T>(
name: string,
fn: () => Promise<T>
): Promise<{ result: T; duration: number }> {
const start = performance.now();
const result = await fn();
const duration = performance.now() - start;
// Store measurement
if (!this.measurements.has(name)) {
this.measurements.set(name, []);
}
this.measurements.get(name)!.push(duration);
return { result, duration };
}
/**
* Gets performance statistics
*/
static getStats(name: string): {
count: number;
min: number;
max: number;
avg: number;
median: number;
} | null {
const measurements = this.measurements.get(name);
if (!measurements || measurements.length === 0) {
return null;
}
const sorted = [...measurements].sort((a, b) => a - b);
const sum = sorted.reduce((a, b) => a + b, 0);
return {
count: sorted.length,
min: sorted[0],
max: sorted[sorted.length - 1],
avg: sum / sorted.length,
median: sorted[Math.floor(sorted.length / 2)]
};
}
/**
* Clears all measurements
*/
static clear(): void {
this.measurements.clear();
}
/**
* Generates a performance report
*/
static generateReport(): string {
let report = 'Performance Report\n==================\n\n';
for (const [name] of this.measurements) {
const stats = this.getStats(name);
if (stats) {
report += `${name}:\n`;
report += ` Executions: ${stats.count}\n`;
report += ` Min: ${stats.min.toFixed(2)}ms\n`;
report += ` Max: ${stats.max.toFixed(2)}ms\n`;
report += ` Avg: ${stats.avg.toFixed(2)}ms\n`;
report += ` Median: ${stats.median.toFixed(2)}ms\n\n`;
}
}
return report;
}
}

View File

@ -1,11 +1,11 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { EInvoice } from '../ts/einvoice.js';
import { InvoiceFormat } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Test circular export/import of corpus files
tap.test('XInvoice should maintain data integrity through export/import cycle', async () => {
tap.test('EInvoice should maintain data integrity through export/import cycle', async () => {
// Get a subset of files for circular testing
const ciiFiles = await findFiles(path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/CII'), '.xml', 3);
const ublFiles = await findFiles(path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/UBL'), '.xml', 3);
@ -66,20 +66,20 @@ async function testCircular(files: string[], exportFormat: string): Promise<{ su
// Read the file
const xmlContent = await fs.readFile(file, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(xmlContent);
// Export to XML
const exportedXml = await xinvoice.exportXml(exportFormat as any);
const exportedXml = await einvoice.exportXml(exportFormat as any);
// Create a new XInvoice from the exported XML
const reimportedXInvoice = await XInvoice.fromXml(exportedXml);
// Create a new EInvoice from the exported XML
const reimportedEInvoice = await EInvoice.fromXml(exportedXml);
// Check that key properties match
const keysMatch =
reimportedXInvoice.from.name === xinvoice.from.name &&
reimportedXInvoice.to.name === xinvoice.to.name &&
reimportedXInvoice.items.length === xinvoice.items.length;
reimportedEInvoice.from.name === einvoice.from.name &&
reimportedEInvoice.to.name === einvoice.to.name &&
reimportedEInvoice.items.length === einvoice.items.length;
if (keysMatch) {
// Success

409
test/test.conversion.ts Normal file
View File

@ -0,0 +1,409 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { EInvoice, EInvoiceFormatError } from '../ts/index.js';
import { InvoiceFormat } from '../ts/interfaces/common.js';
import { TestFileHelpers, TestFileCategories, PerformanceUtils, TestInvoiceFactory } from './test-utils.js';
import * as path from 'path';
/**
* Cross-format conversion test suite
*/
// Test conversion between CII and UBL using paired files
tap.test('Conversion - CII to UBL using XML-Rechnung pairs', async () => {
// Get matching CII and UBL files
const ciiFiles = await TestFileHelpers.getTestFiles(TestFileCategories.CII_XMLRECHNUNG, '*.xml');
const ublFiles = await TestFileHelpers.getTestFiles(TestFileCategories.UBL_XMLRECHNUNG, '*.xml');
// Find paired files (same base name)
const pairs: Array<{cii: string, ubl: string, name: string}> = [];
for (const ciiFile of ciiFiles) {
const baseName = path.basename(ciiFile).replace('.cii.xml', '');
const matchingUbl = ublFiles.find(ubl =>
path.basename(ubl).startsWith(baseName) && ubl.endsWith('.ubl.xml')
);
if (matchingUbl) {
pairs.push({ cii: ciiFile, ubl: matchingUbl, name: baseName });
}
}
console.log(`Found ${pairs.length} CII/UBL pairs for conversion testing`);
let successCount = 0;
const conversionIssues: string[] = [];
for (const pair of pairs.slice(0, 5)) { // Test first 5 pairs
try {
// Load CII invoice
const ciiBuffer = await TestFileHelpers.loadTestFile(pair.cii);
const ciiInvoice = await EInvoice.fromXml(ciiBuffer.toString('utf-8'));
// Convert to UBL
const { result: ublXml, duration } = await PerformanceUtils.measure(
'cii-to-ubl',
async () => ciiInvoice.exportXml('ubl')
);
expect(ublXml).toBeTruthy();
expect(ublXml).toInclude('xmlns:cbc=');
expect(ublXml).toInclude('xmlns:cac=');
// Load the converted UBL back
const convertedInvoice = await EInvoice.fromXml(ublXml);
// Verify key fields are preserved
verifyFieldMapping(ciiInvoice, convertedInvoice, pair.name);
successCount++;
console.log(`${pair.name}: CII→UBL conversion successful (${duration.toFixed(2)}ms)`);
} catch (error) {
const issue = `${pair.name}: ${error.message}`;
conversionIssues.push(issue);
console.log(`${issue}`);
}
}
console.log(`\nConversion Summary: ${successCount}/${pairs.length} successful`);
if (conversionIssues.length > 0) {
console.log('Issues:', conversionIssues);
}
});
// Test conversion from UBL to CII
tap.test('Conversion - UBL to CII reverse conversion', async () => {
const ublFiles = await TestFileHelpers.getTestFiles(TestFileCategories.UBL_XMLRECHNUNG, '*.xml');
console.log(`Testing UBL to CII conversion with ${ublFiles.length} files`);
for (const file of ublFiles.slice(0, 3)) {
const fileName = path.basename(file);
try {
const ublBuffer = await TestFileHelpers.loadTestFile(file);
const ublInvoice = await EInvoice.fromXml(ublBuffer.toString('utf-8'));
// Skip if detected as XRechnung (might have special requirements)
if (ublInvoice.getFormat() === InvoiceFormat.XRECHNUNG) {
console.log(`${fileName}: Skipping XRechnung-specific file`);
continue;
}
// Convert to CII (Factur-X)
const ciiXml = await ublInvoice.exportXml('facturx');
expect(ciiXml).toBeTruthy();
expect(ciiXml).toInclude('CrossIndustryInvoice');
expect(ciiXml).toInclude('ExchangedDocument');
// Verify round-trip
const ciiInvoice = await EInvoice.fromXml(ciiXml);
expect(ciiInvoice.invoiceId).toEqual(ublInvoice.invoiceId);
console.log(`${fileName}: UBL→CII conversion successful`);
} catch (error) {
if (error instanceof EInvoiceFormatError) {
console.log(`${fileName}: Format error - ${error.message}`);
if (error.unsupportedFeatures) {
console.log(` Unsupported features: ${error.unsupportedFeatures.join(', ')}`);
}
} else {
console.log(`${fileName}: ${error.message}`);
}
}
}
});
// Test ZUGFeRD to XRechnung conversion
tap.test('Conversion - ZUGFeRD to XRechnung format', async () => {
const zugferdPdfs = await TestFileHelpers.getTestFiles(TestFileCategories.ZUGFERD_V2_CORRECT, '*.pdf');
let tested = 0;
for (const file of zugferdPdfs.slice(0, 3)) {
const fileName = path.basename(file);
try {
// Extract from PDF
const pdfBuffer = await TestFileHelpers.loadTestFile(file);
const zugferdInvoice = await EInvoice.fromPdf(pdfBuffer);
// Convert to XRechnung
const xrechnungXml = await zugferdInvoice.exportXml('xrechnung');
expect(xrechnungXml).toBeTruthy();
// XRechnung should be UBL format with specific extensions
if (xrechnungXml.includes('Invoice xmlns')) {
expect(xrechnungXml).toInclude('CustomizationID');
expect(xrechnungXml).toInclude('urn:cen.eu:en16931');
}
tested++;
console.log(`${fileName}: ZUGFeRD→XRechnung conversion successful`);
} catch (error) {
console.log(`${fileName}: Conversion not available - ${error.message}`);
}
}
if (tested === 0) {
console.log('Note: ZUGFeRD to XRechnung conversion may need implementation');
}
});
// Test data loss detection during conversion
tap.test('Conversion - Data loss detection and reporting', async () => {
// Create a complex invoice with all possible fields
const complexInvoice = new EInvoice();
Object.assign(complexInvoice, TestInvoiceFactory.createComplexInvoice());
// Add format-specific fields
complexInvoice.buyerReference = 'PO-2024-12345';
complexInvoice.electronicAddress = {
scheme: '0088',
value: '1234567890123'
};
complexInvoice.notes = [
'Special handling required',
'Express delivery requested',
'Contact buyer before delivery'
];
// Generate source XML
const sourceXml = await complexInvoice.exportXml('facturx');
await complexInvoice.loadXml(sourceXml);
// Test conversions and check for data loss
const formats: Array<{from: string, to: string}> = [
{ from: 'facturx', to: 'ubl' },
{ from: 'facturx', to: 'xrechnung' },
{ from: 'facturx', to: 'zugferd' }
];
for (const conversion of formats) {
console.log(`\nTesting ${conversion.from}${conversion.to} conversion:`);
try {
const convertedXml = await complexInvoice.exportXml(conversion.to as any);
const convertedInvoice = await EInvoice.fromXml(convertedXml);
// Check for data preservation
const issues = checkDataPreservation(complexInvoice, convertedInvoice);
if (issues.length === 0) {
console.log(`✓ All data preserved in ${conversion.to} format`);
} else {
console.log(`⚠ Data loss detected in ${conversion.to} format:`);
issues.forEach(issue => console.log(` - ${issue}`));
}
} catch (error) {
console.log(`✗ Conversion failed: ${error.message}`);
}
}
});
// Test edge cases in conversion
tap.test('Conversion - Edge cases and special characters', async () => {
const edgeCaseInvoice = new EInvoice();
Object.assign(edgeCaseInvoice, TestInvoiceFactory.createMinimalInvoice());
// Add edge case data
edgeCaseInvoice.from.name = 'Müller & Söhne GmbH & Co. KG';
edgeCaseInvoice.to.name = 'L\'Entreprise Française S.à.r.l.';
edgeCaseInvoice.items[0].name = 'Product with "quotes" and <tags>';
edgeCaseInvoice.notes = ['Note with € symbol', 'Japanese: こんにちは'];
// Test conversion with special characters
const formats = ['facturx', 'ubl', 'xrechnung'] as const;
for (const format of formats) {
try {
const xml = await edgeCaseInvoice.exportXml(format);
// Verify special characters are properly encoded
expect(xml).toInclude('Müller');
expect(xml).toInclude('Française');
expect(xml).toContain('&quot;'); // Encoded quotes
expect(xml).toContain('&lt;'); // Encoded less-than
// Verify it can be parsed back
const parsed = await EInvoice.fromXml(xml);
expect(parsed.from.name).toEqual('Müller & Söhne GmbH & Co. KG');
console.log(`${format}: Special characters handled correctly`);
} catch (error) {
console.log(`${format}: Failed with special characters - ${error.message}`);
}
}
});
// Test batch conversion performance
tap.test('Conversion - Batch conversion performance', async () => {
const files = await TestFileHelpers.getTestFiles(TestFileCategories.UBL_XMLRECHNUNG, '*.xml');
const batchSize = Math.min(10, files.length);
console.log(`Testing batch conversion of ${batchSize} files`);
const startTime = performance.now();
const results = await Promise.all(
files.slice(0, batchSize).map(async (file) => {
try {
const buffer = await TestFileHelpers.loadTestFile(file);
const invoice = await EInvoice.fromXml(buffer.toString('utf-8'));
const converted = await invoice.exportXml('facturx');
return { success: true, size: converted.length };
} catch (error) {
return { success: false, error: error.message };
}
})
);
const duration = performance.now() - startTime;
const successCount = results.filter(r => r.success).length;
console.log(`✓ Batch conversion completed in ${duration.toFixed(2)}ms`);
console.log(` Success rate: ${successCount}/${batchSize}`);
console.log(` Average time per conversion: ${(duration / batchSize).toFixed(2)}ms`);
expect(duration / batchSize).toBeLessThan(500); // Should be under 500ms per conversion
});
// Test format-specific extensions preservation
tap.test('Conversion - Format-specific extensions', async () => {
// This tests that format-specific extensions don't break conversion
const extensionTests = [
{
name: 'XRechnung BuyerReference',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xrechnung:cius:2.0</CustomizationID>
<ID>123</ID>
<BuyerReference>04011000-12345-03</BuyerReference>
</Invoice>`
}
];
for (const test of extensionTests) {
try {
const invoice = await EInvoice.fromXml(test.xml);
// Convert to CII
const ciiXml = await invoice.exportXml('facturx');
expect(ciiXml).toBeTruthy();
// Convert back to UBL
const ciiInvoice = await EInvoice.fromXml(ciiXml);
const ublXml = await ciiInvoice.exportXml('ubl');
// Check if buyer reference survived
if (invoice.buyerReference) {
expect(ublXml).toInclude(invoice.buyerReference);
}
console.log(`${test.name}: Extension preserved through conversion`);
} catch (error) {
console.log(`${test.name}: ${error.message}`);
}
}
});
// Test conversion error handling
tap.test('Conversion - Error handling and recovery', async () => {
// Test with minimal invalid invoice
const invalidInvoice = new EInvoice();
invalidInvoice.id = 'TEST-INVALID';
// Missing required fields like from, to, items
try {
await invalidInvoice.exportXml('facturx');
expect.fail('Should have thrown an error for invalid invoice');
} catch (error) {
console.log(`✓ Invalid invoice error caught: ${error.message}`);
if (error instanceof EInvoiceFormatError) {
console.log(` Compatibility report:\n${error.getCompatibilityReport()}`);
}
}
});
// Performance summary for conversions
tap.test('Conversion - Performance Summary', async () => {
const conversionStats = PerformanceUtils.getStats('cii-to-ubl');
if (conversionStats) {
console.log('\nConversion Performance:');
console.log(`CII to UBL conversions: ${conversionStats.count}`);
console.log(`Average time: ${conversionStats.avg.toFixed(2)}ms`);
console.log(`Min/Max: ${conversionStats.min.toFixed(2)}ms / ${conversionStats.max.toFixed(2)}ms`);
// Conversions should be reasonably fast
expect(conversionStats.avg).toBeLessThan(100);
}
});
// Helper function to verify field mapping
function verifyFieldMapping(source: EInvoice, converted: EInvoice, testName: string): void {
const criticalFields = [
{ field: 'invoiceId', name: 'Invoice ID' },
{ field: 'date', name: 'Invoice Date' },
{ field: 'currency', name: 'Currency' }
];
for (const check of criticalFields) {
const sourceVal = source[check.field as keyof EInvoice];
const convertedVal = converted[check.field as keyof EInvoice];
if (sourceVal !== convertedVal) {
console.log(`${check.name} mismatch: ${sourceVal}${convertedVal}`);
}
}
// Check seller/buyer
if (source.from.name !== converted.from.name) {
console.log(` ⚠ Seller name mismatch: ${source.from.name}${converted.from.name}`);
}
if (source.to.name !== converted.to.name) {
console.log(` ⚠ Buyer name mismatch: ${source.to.name}${converted.to.name}`);
}
// Check items count
if (source.items.length !== converted.items.length) {
console.log(` ⚠ Items count mismatch: ${source.items.length}${converted.items.length}`);
}
}
// Helper function to check data preservation
function checkDataPreservation(source: EInvoice, converted: EInvoice): string[] {
const issues: string[] = [];
// Check basic fields
if (source.invoiceId !== converted.invoiceId) {
issues.push(`Invoice ID changed: ${source.invoiceId}${converted.invoiceId}`);
}
if (source.buyerReference && source.buyerReference !== converted.buyerReference) {
issues.push(`Buyer reference lost or changed`);
}
if (source.notes && source.notes.length !== converted.notes?.length) {
issues.push(`Notes count changed: ${source.notes.length}${converted.notes?.length || 0}`);
}
if (source.electronicAddress && !converted.electronicAddress) {
issues.push(`Electronic address lost`);
}
// Check payment details
if (source.paymentOptions?.sepaConnection?.iban !== converted.paymentOptions?.sepaConnection?.iban) {
issues.push(`IBAN changed or lost`);
}
return issues;
}
tap.start();

View File

@ -13,7 +13,7 @@ tap.test('Run all corpus tests', async () => {
// Generate a summary report from existing results
try {
// Create a simple summary
const summary = `# XInvoice Corpus Testing Summary
const summary = `# EInvoice Corpus Testing Summary
Generated on: ${new Date().toISOString()}

View File

@ -1,11 +1,11 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { EInvoice } from '../ts/einvoice.js';
import { ValidationLevel } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Test for XInvoice class functionality
tap.test('XInvoice should load XML correctly', async () => {
// Test for EInvoice class functionality
tap.test('EInvoice should load XML correctly', async () => {
// Create a sample XML string
const sampleXml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
@ -68,16 +68,16 @@ tap.test('XInvoice should load XML correctly', async () => {
const xmlPath = path.join(testDir, 'sample-invoice.xml');
await fs.writeFile(xmlPath, sampleXml);
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(sampleXml);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(sampleXml);
// Check that the XInvoice instance has the expected properties
expect(xinvoice.id).toEqual('INV-2023-001');
expect(xinvoice.from.name).toEqual('Supplier Company');
expect(xinvoice.to.name).toEqual('Customer Company');
// Check that the EInvoice instance has the expected properties
expect(einvoice.id).toEqual('INV-2023-001');
expect(einvoice.from.name).toEqual('Supplier Company');
expect(einvoice.to.name).toEqual('Customer Company');
});
tap.test('XInvoice should export XML correctly', async () => {
tap.test('EInvoice should export XML correctly', async () => {
// Create a sample XML string
const sampleXml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
@ -134,11 +134,11 @@ tap.test('XInvoice should export XML correctly', async () => {
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`;
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(sampleXml);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(sampleXml);
// Export XML
const exportedXml = await xinvoice.exportXml('facturx');
const exportedXml = await einvoice.exportXml('facturx');
// Check that the exported XML contains expected elements
expect(exportedXml).toInclude('CrossIndustryInvoice');

View File

@ -1,31 +1,31 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { EInvoice } from '../ts/einvoice.js';
import { ValidationLevel } from '../ts/interfaces/common.js';
import type { ExportFormat } from '../ts/interfaces/common.js';
// Basic XInvoice tests
tap.test('XInvoice should have the correct default properties', async () => {
const xinvoice = new XInvoice();
// Basic EInvoice tests
tap.test('EInvoice should have the correct default properties', async () => {
const einvoice = new EInvoice();
expect(xinvoice.type).toEqual('invoice');
expect(xinvoice.invoiceType).toEqual('debitnote');
expect(xinvoice.status).toEqual('invoice');
expect(xinvoice.from).toBeTruthy();
expect(xinvoice.to).toBeTruthy();
expect(xinvoice.items).toBeArray();
expect(xinvoice.currency).toEqual('EUR');
expect(einvoice.type).toEqual('invoice');
expect(einvoice.invoiceType).toEqual('debitnote');
expect(einvoice.status).toEqual('invoice');
expect(einvoice.from).toBeTruthy();
expect(einvoice.to).toBeTruthy();
expect(einvoice.items).toBeArray();
expect(einvoice.currency).toEqual('EUR');
});
// Test XML export functionality
tap.test('XInvoice should export XML in the correct format', async () => {
const xinvoice = new XInvoice();
xinvoice.id = 'TEST-XML-EXPORT';
xinvoice.invoiceId = 'TEST-XML-EXPORT';
xinvoice.from.name = 'Test Seller';
xinvoice.to.name = 'Test Buyer';
tap.test('EInvoice should export XML in the correct format', async () => {
const einvoice = new EInvoice();
einvoice.id = 'TEST-XML-EXPORT';
einvoice.invoiceId = 'TEST-XML-EXPORT';
einvoice.from.name = 'Test Seller';
einvoice.to.name = 'Test Buyer';
// Add an item
xinvoice.items.push({
einvoice.items.push({
position: 1,
name: 'Test Product',
articleNumber: 'TP-001',
@ -36,7 +36,7 @@ tap.test('XInvoice should export XML in the correct format', async () => {
});
// Export as Factur-X
const xml = await xinvoice.exportXml('facturx');
const xml = await einvoice.exportXml('facturx');
// Check that the XML contains the expected elements
expect(xml).toInclude('CrossIndustryInvoice');
@ -47,7 +47,7 @@ tap.test('XInvoice should export XML in the correct format', async () => {
});
// Test XML loading functionality
tap.test('XInvoice should load XML correctly', async () => {
tap.test('EInvoice should load XML correctly', async () => {
// Create a sample XML string
const sampleXml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
@ -94,20 +94,20 @@ tap.test('XInvoice should load XML correctly', async () => {
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`;
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(sampleXml);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(sampleXml);
// Check that the XInvoice instance has the expected properties
expect(xinvoice.id).toEqual('TEST-XML-LOAD');
expect(xinvoice.from.name).toEqual('XML Seller');
expect(xinvoice.to.name).toEqual('XML Buyer');
expect(xinvoice.currency).toEqual('EUR');
// Check that the EInvoice instance has the expected properties
expect(einvoice.id).toEqual('TEST-XML-LOAD');
expect(einvoice.from.name).toEqual('XML Seller');
expect(einvoice.to.name).toEqual('XML Buyer');
expect(einvoice.currency).toEqual('EUR');
});
// Test circular encoding/decoding
tap.test('XInvoice should maintain data integrity through export/import cycle', async () => {
tap.test('EInvoice should maintain data integrity through export/import cycle', async () => {
// Create a sample invoice
const originalInvoice = new XInvoice();
const originalInvoice = new EInvoice();
originalInvoice.id = 'TEST-CIRCULAR';
originalInvoice.invoiceId = 'TEST-CIRCULAR';
originalInvoice.from.name = 'Circular Seller';
@ -127,8 +127,8 @@ tap.test('XInvoice should maintain data integrity through export/import cycle',
// Export as Factur-X
const xml = await originalInvoice.exportXml('facturx');
// Create a new XInvoice from the XML
const importedInvoice = await XInvoice.fromXml(xml);
// Create a new EInvoice from the XML
const importedInvoice = await EInvoice.fromXml(xml);
// Check that key properties match
expect(importedInvoice.id).toEqual(originalInvoice.id);
@ -143,21 +143,21 @@ tap.test('XInvoice should maintain data integrity through export/import cycle',
});
// Test validation
tap.test('XInvoice should validate XML correctly', async () => {
const xinvoice = new XInvoice();
xinvoice.id = 'TEST-VALIDATION';
xinvoice.invoiceId = 'TEST-VALIDATION';
xinvoice.from.name = 'Validation Seller';
xinvoice.to.name = 'Validation Buyer';
tap.test('EInvoice should validate XML correctly', async () => {
const einvoice = new EInvoice();
einvoice.id = 'TEST-VALIDATION';
einvoice.invoiceId = 'TEST-VALIDATION';
einvoice.from.name = 'Validation Seller';
einvoice.to.name = 'Validation Buyer';
// Export as Factur-X
const xml = await xinvoice.exportXml('facturx');
const xml = await einvoice.exportXml('facturx');
// Set the XML string for validation
xinvoice['xmlString'] = xml;
einvoice['xmlString'] = xml;
// Validate the XML
const result = await xinvoice.validate(ValidationLevel.SYNTAX);
const result = await einvoice.validate(ValidationLevel.SYNTAX);
// Check that validation passed
expect(result.valid).toBeTrue();

394
test/test.error-handling.ts Normal file
View File

@ -0,0 +1,394 @@
import { tap, expect } from '@push.rocks/tapbundle';
import {
EInvoice,
EInvoiceError,
EInvoiceParsingError,
EInvoiceValidationError,
EInvoicePDFError,
EInvoiceFormatError,
ErrorRecovery,
ErrorContext
} from '../ts/index.js';
import { ValidationLevel } from '../ts/interfaces/common.js';
import { TestFileHelpers, TestFileCategories } from './test-utils.js';
import * as path from 'path';
/**
* Error handling and recovery test suite
*/
// Test EInvoiceParsingError functionality
tap.test('Error Handling - Parsing errors with location info', async () => {
const malformedXml = `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>123</ID>
<IssueDate>2024-01-01
<InvoiceLine>
<ID>1</ID>
</InvoiceLine>
</Invoice>`;
try {
await EInvoice.fromXml(malformedXml);
expect.fail('Should have thrown a parsing error');
} catch (error) {
expect(error).toBeInstanceOf(EInvoiceParsingError);
if (error instanceof EInvoiceParsingError) {
console.log('✓ Parsing error caught correctly');
console.log(` Message: ${error.message}`);
console.log(` Code: ${error.code}`);
console.log(` Detailed: ${error.getDetailedMessage()}`);
// Check error properties
expect(error.code).toEqual('PARSE_ERROR');
expect(error.name).toEqual('EInvoiceParsingError');
expect(error.details).toBeTruthy();
}
}
});
// Test XML recovery mechanisms
tap.test('Error Handling - XML recovery for common issues', async () => {
// Test 1: XML with BOM
const xmlWithBOM = '\ufeff<?xml version="1.0"?><Invoice><ID>123</ID></Invoice>';
const bomError = new EInvoiceParsingError('BOM detected', { xmlSnippet: xmlWithBOM.substring(0, 50) });
const bomRecovery = await ErrorRecovery.attemptXMLRecovery(xmlWithBOM, bomError);
expect(bomRecovery.success).toBeTruthy();
expect(bomRecovery.cleanedXml).toBeTruthy();
expect(bomRecovery.cleanedXml!.charCodeAt(0)).not.toEqual(0xFEFF);
console.log('✓ BOM removal recovery successful');
// Test 2: Unescaped ampersands
const xmlWithAmpersand = '<?xml version="1.0"?><Invoice><Name>Smith & Jones Ltd</Name></Invoice>';
const ampError = new EInvoiceParsingError('Unescaped ampersand', {});
const ampRecovery = await ErrorRecovery.attemptXMLRecovery(xmlWithAmpersand, ampError);
expect(ampRecovery.success).toBeTruthy();
if (ampRecovery.cleanedXml) {
expect(ampRecovery.cleanedXml).toInclude('&amp;');
console.log('✓ Ampersand escaping recovery successful');
}
});
// Test validation error handling
tap.test('Error Handling - Validation errors with detailed reports', async () => {
const invoice = new EInvoice();
try {
await invoice.validate(ValidationLevel.BUSINESS);
expect.fail('Should have thrown validation error for empty invoice');
} catch (error) {
expect(error).toBeInstanceOf(EInvoiceValidationError);
if (error instanceof EInvoiceValidationError) {
console.log('✓ Validation error caught');
console.log('Validation Report:');
console.log(error.getValidationReport());
// Check error filtering
const errors = error.getErrorsBySeverity('error');
expect(errors.length).toBeGreaterThan(0);
const warnings = error.getErrorsBySeverity('warning');
console.log(` Errors: ${errors.length}, Warnings: ${warnings.length}`);
}
}
});
// Test PDF error handling
tap.test('Error Handling - PDF operation errors', async () => {
// Test extraction error
const extractError = new EInvoicePDFError(
'No XML found in PDF',
'extract',
{
pdfInfo: {
filename: 'test.pdf',
size: 1024 * 1024,
pageCount: 10
}
}
);
console.log('PDF Extraction Error:');
console.log(` Message: ${extractError.message}`);
console.log(` Operation: ${extractError.operation}`);
console.log(' Recovery suggestions:');
extractError.getRecoverySuggestions().forEach(s => console.log(` - ${s}`));
expect(extractError.code).toEqual('PDF_EXTRACT_ERROR');
expect(extractError.getRecoverySuggestions().length).toBeGreaterThan(0);
// Test embed error
const embedError = new EInvoicePDFError(
'Failed to embed XML',
'embed',
{ xmlLength: 50000 }
);
expect(embedError.code).toEqual('PDF_EMBED_ERROR');
expect(embedError.getRecoverySuggestions()).toContain('Try with a smaller XML payload');
});
// Test format errors
tap.test('Error Handling - Format conversion errors', async () => {
const formatError = new EInvoiceFormatError(
'Cannot convert invoice: incompatible fields',
{
sourceFormat: 'fatturapa',
targetFormat: 'xrechnung',
unsupportedFeatures: [
'Italian-specific tax codes',
'PEC electronic address format',
'Bollo virtuale'
]
}
);
console.log('Format Conversion Error:');
console.log(formatError.getCompatibilityReport());
expect(formatError.sourceFormat).toEqual('fatturapa');
expect(formatError.targetFormat).toEqual('xrechnung');
expect(formatError.unsupportedFeatures?.length).toEqual(3);
});
// Test error context builder
tap.test('Error Handling - Error context enrichment', async () => {
const context = new ErrorContext()
.add('operation', 'invoice_validation')
.add('invoiceId', 'INV-2024-001')
.add('format', 'facturx')
.addTimestamp()
.addEnvironment()
.build();
expect(context.operation).toEqual('invoice_validation');
expect(context.invoiceId).toEqual('INV-2024-001');
expect(context.timestamp).toBeTruthy();
expect(context.environment).toBeTruthy();
expect(context.environment.nodeVersion).toBeTruthy();
console.log('✓ Error context built successfully');
console.log(` Context keys: ${Object.keys(context).join(', ')}`);
});
// Test error propagation through the stack
tap.test('Error Handling - Error propagation and chaining', async () => {
// Create a chain of errors
const rootCause = new Error('Database connection failed');
const serviceError = new EInvoiceError(
'Failed to load invoice template',
'TEMPLATE_ERROR',
{ templateId: 'facturx-v2' },
rootCause
);
const userError = new EInvoicePDFError(
'Cannot generate PDF invoice',
'create',
{ invoiceId: 'INV-001' },
serviceError
);
console.log('Error Chain:');
console.log(` User sees: ${userError.message}`);
console.log(` Caused by: ${userError.cause?.message}`);
console.log(` Root cause: ${(userError.cause as EInvoiceError)?.cause?.message}`);
expect(userError.cause).toBeTruthy();
expect((userError.cause as EInvoiceError).cause).toBeTruthy();
});
// Test recovery from real corpus errors
tap.test('Error Handling - Recovery from corpus parsing errors', async () => {
// Try to load files that might have issues
const problematicFiles = [
'test/assets/corpus/other/eicar.cii.xml',
'test/assets/corpus/other/eicar.ubl.xml'
];
for (const filePath of problematicFiles) {
try {
const fileBuffer = await TestFileHelpers.loadTestFile(filePath);
const xmlString = fileBuffer.toString('utf-8');
const invoice = await EInvoice.fromXml(xmlString);
console.log(`${path.basename(filePath)}: Loaded successfully (no error to handle)`);
} catch (error) {
if (error instanceof EInvoiceParsingError) {
console.log(`${path.basename(filePath)}: Parsing error handled`);
// Attempt recovery
const recovery = await ErrorRecovery.attemptXMLRecovery(
error.details?.xmlString || '',
error
);
if (recovery.success) {
console.log(` Recovery: ${recovery.message}`);
}
} else {
console.log(`${path.basename(filePath)}: Error handled - ${error.message}`);
}
}
}
});
// Test concurrent error scenarios
tap.test('Error Handling - Concurrent error handling', async () => {
const errorScenarios = [
async () => {
throw new EInvoiceParsingError('Scenario 1: Invalid XML', { line: 10, column: 5 });
},
async () => {
throw new EInvoiceValidationError('Scenario 2: Validation failed', [
{ code: 'BR-01', message: 'Invoice number required' }
]);
},
async () => {
throw new EInvoicePDFError('Scenario 3: PDF corrupted', 'extract');
},
async () => {
throw new EInvoiceFormatError('Scenario 4: Format unsupported', {
sourceFormat: 'custom',
targetFormat: 'xrechnung'
});
}
];
const results = await Promise.allSettled(errorScenarios.map(fn => fn()));
let errorTypeCounts: Record<string, number> = {};
results.forEach((result, index) => {
if (result.status === 'rejected') {
const errorType = result.reason.constructor.name;
errorTypeCounts[errorType] = (errorTypeCounts[errorType] || 0) + 1;
console.log(`✓ Scenario ${index + 1}: ${errorType} handled`);
}
});
expect(Object.keys(errorTypeCounts).length).toEqual(4);
console.log('\nError type distribution:', errorTypeCounts);
});
// Test error serialization for logging
tap.test('Error Handling - Error serialization', async () => {
const error = new EInvoiceValidationError(
'Multiple validation failures',
[
{ code: 'BR-01', message: 'Invoice number missing', location: '/Invoice/ID' },
{ code: 'BR-05', message: 'Invalid date format', location: '/Invoice/IssueDate' },
{ code: 'BR-CL-01', message: 'Invalid currency code', location: '/Invoice/DocumentCurrencyCode' }
],
{ invoiceId: 'TEST-001', validationLevel: 'BUSINESS' }
);
// Test JSON serialization
const serialized = JSON.stringify({
name: error.name,
message: error.message,
code: error.code,
validationErrors: error.validationErrors,
details: error.details
}, null, 2);
const parsed = JSON.parse(serialized);
expect(parsed.name).toEqual('EInvoiceValidationError');
expect(parsed.code).toEqual('VALIDATION_ERROR');
expect(parsed.validationErrors.length).toEqual(3);
console.log('✓ Error serializes correctly for logging');
console.log('Serialized error sample:');
console.log(serialized.substring(0, 200) + '...');
});
// Test error recovery strategies
tap.test('Error Handling - Recovery strategy selection', async () => {
// Simulate different error scenarios and recovery strategies
const scenarios = [
{
name: 'Missing closing tag',
xml: '<?xml version="1.0"?><Invoice><ID>123</ID>',
expectedRecovery: false // Hard to recover automatically
},
{
name: 'Extra whitespace',
xml: '<?xml version="1.0"?> \n\n <Invoice><ID>123</ID></Invoice>',
expectedRecovery: true
},
{
name: 'Wrong encoding declaration',
xml: '<?xml version="1.0" encoding="UTF-16"?><Invoice><ID>123</ID></Invoice>',
expectedRecovery: true
}
];
for (const scenario of scenarios) {
try {
await EInvoice.fromXml(scenario.xml);
console.log(`${scenario.name}: No error occurred`);
} catch (error) {
if (error instanceof EInvoiceParsingError) {
const recovery = await ErrorRecovery.attemptXMLRecovery(scenario.xml, error);
const result = recovery.success ? '✓' : '✗';
console.log(`${result} ${scenario.name}: Recovery ${recovery.success ? 'succeeded' : 'failed'}`);
if (scenario.expectedRecovery !== recovery.success) {
console.log(` Note: Expected recovery=${scenario.expectedRecovery}, got=${recovery.success}`);
}
}
}
}
});
// Test error metrics collection
tap.test('Error Handling - Error metrics and patterns', async () => {
const errorMetrics = {
total: 0,
byType: {} as Record<string, number>,
byCode: {} as Record<string, number>,
recoveryAttempts: 0,
recoverySuccesses: 0
};
// Simulate processing multiple files
const testFiles = await TestFileHelpers.getTestFiles(TestFileCategories.EN16931_UBL_INVOICE, 'BR-*.xml');
for (const file of testFiles.slice(0, 10)) {
try {
const buffer = await TestFileHelpers.loadTestFile(file);
const invoice = await EInvoice.fromXml(buffer.toString('utf-8'));
await invoice.validate(ValidationLevel.BUSINESS);
} catch (error) {
errorMetrics.total++;
if (error instanceof EInvoiceError) {
const type = error.constructor.name;
errorMetrics.byType[type] = (errorMetrics.byType[type] || 0) + 1;
errorMetrics.byCode[error.code] = (errorMetrics.byCode[error.code] || 0) + 1;
// Try recovery for parsing errors
if (error instanceof EInvoiceParsingError) {
errorMetrics.recoveryAttempts++;
const recovery = await ErrorRecovery.attemptXMLRecovery('', error);
if (recovery.success) {
errorMetrics.recoverySuccesses++;
}
}
}
}
}
console.log('\nError Metrics Summary:');
console.log(` Total errors: ${errorMetrics.total}`);
console.log(` Error types:`, errorMetrics.byType);
console.log(` Recovery rate: ${errorMetrics.recoveryAttempts > 0
? (errorMetrics.recoverySuccesses / errorMetrics.recoveryAttempts * 100).toFixed(1)
: 0}%`);
});
tap.start();

View File

@ -1,11 +1,11 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { EInvoice } from '../ts/einvoice.js';
import { InvoiceFormat } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Test a focused subset of corpus files
tap.test('XInvoice should handle a focused subset of corpus files', async () => {
tap.test('EInvoice should handle a focused subset of corpus files', async () => {
// Get a small subset of files for focused testing
const ciiFiles = await findFiles(path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/CII'), '.xml', 5);
const ublFiles = await findFiles(path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/UBL'), '.xml', 5);
@ -55,13 +55,13 @@ async function testXmlFile(file: string, expectedFormat: InvoiceFormat): Promise
// Read the file
const xmlContent = await fs.readFile(file, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(xmlContent);
// Check that the XInvoice instance has the expected properties
if (xinvoice && xinvoice.from && xinvoice.to && xinvoice.items) {
// Check that the EInvoice instance has the expected properties
if (einvoice && einvoice.from && einvoice.to && einvoice.items) {
// Check that the format is detected correctly
const format = xinvoice.getFormat();
const format = einvoice.getFormat();
const isCorrectFormat = format === expectedFormat ||
(expectedFormat === InvoiceFormat.CII && format === InvoiceFormat.FACTURX) ||
(expectedFormat === InvoiceFormat.FACTURX && format === InvoiceFormat.CII) ||
@ -76,14 +76,14 @@ async function testXmlFile(file: string, expectedFormat: InvoiceFormat): Promise
exportFormat = 'xrechnung';
}
const exportedXml = await xinvoice.exportXml(exportFormat as any);
const exportedXml = await einvoice.exportXml(exportFormat as any);
if (exportedXml) {
console.log('✅ Success: File loaded, format detected correctly, and exported successfully');
console.log(`Format: ${format}`);
console.log(`From: ${xinvoice.from.name}`);
console.log(`To: ${xinvoice.to.name}`);
console.log(`Items: ${xinvoice.items.length}`);
console.log(`From: ${einvoice.from.name}`);
console.log(`To: ${einvoice.to.name}`);
console.log(`Items: ${einvoice.items.length}`);
// Save the exported XML for inspection
const testDir = path.join(process.cwd(), 'test', 'output', 'focused');
@ -176,23 +176,23 @@ async function testPdfFile(file: string): Promise<void> {
// Save the extracted XML for inspection
await fs.writeFile(path.join(testDir, `${path.basename(file)}-extracted.xml`), xmlContent);
// Try to create XInvoice from the extracted XML
// Try to create EInvoice from the extracted XML
try {
const xinvoice = await XInvoice.fromXml(xmlContent);
const einvoice = await EInvoice.fromXml(xmlContent);
if (xinvoice && xinvoice.from && xinvoice.to && xinvoice.items) {
console.log('✅ Successfully created XInvoice from extracted XML');
console.log(`Format: ${xinvoice.getFormat()}`);
console.log(`From: ${xinvoice.from.name}`);
console.log(`To: ${xinvoice.to.name}`);
console.log(`Items: ${xinvoice.items.length}`);
if (einvoice && einvoice.from && einvoice.to && einvoice.items) {
console.log('✅ Successfully created EInvoice from extracted XML');
console.log(`Format: ${einvoice.getFormat()}`);
console.log(`From: ${einvoice.from.name}`);
console.log(`To: ${einvoice.to.name}`);
console.log(`Items: ${einvoice.items.length}`);
// Try to export the invoice back to XML
try {
const exportedXml = await xinvoice.exportXml('facturx');
const exportedXml = await einvoice.exportXml('facturx');
if (exportedXml) {
console.log('✅ Successfully exported XInvoice back to XML');
console.log('✅ Successfully exported EInvoice back to XML');
// Save the exported XML for inspection
await fs.writeFile(path.join(testDir, `${path.basename(file)}-reexported.xml`), exportedXml);
@ -203,30 +203,30 @@ async function testPdfFile(file: string): Promise<void> {
console.log(`❌ Export error: ${exportError.message}`);
}
} else {
console.log('❌ Missing required properties in created XInvoice');
console.log('❌ Missing required properties in created EInvoice');
}
} catch (xmlError) {
console.log(`❌ Error creating XInvoice from extracted XML: ${xmlError.message}`);
console.log(`❌ Error creating EInvoice from extracted XML: ${xmlError.message}`);
}
} else {
console.log('❌ No XML found in PDF');
}
// Try to create XInvoice directly from PDF
// Try to create EInvoice directly from PDF
try {
const xinvoice = await XInvoice.fromPdf(pdfBuffer);
const einvoice = await EInvoice.fromPdf(pdfBuffer);
if (xinvoice && xinvoice.from && xinvoice.to && xinvoice.items) {
console.log('✅ Successfully created XInvoice directly from PDF');
console.log(`Format: ${xinvoice.getFormat()}`);
console.log(`From: ${xinvoice.from.name}`);
console.log(`To: ${xinvoice.to.name}`);
console.log(`Items: ${xinvoice.items.length}`);
if (einvoice && einvoice.from && einvoice.to && einvoice.items) {
console.log('✅ Successfully created EInvoice directly from PDF');
console.log(`Format: ${einvoice.getFormat()}`);
console.log(`From: ${einvoice.from.name}`);
console.log(`To: ${einvoice.to.name}`);
console.log(`Items: ${einvoice.items.length}`);
} else {
console.log('❌ Missing required properties in created XInvoice');
console.log('❌ Missing required properties in created EInvoice');
}
} catch (pdfError) {
console.log(`❌ Error creating XInvoice directly from PDF: ${pdfError.message}`);
console.log(`❌ Error creating EInvoice directly from PDF: ${pdfError.message}`);
}
} catch (error) {
console.log(`❌ Error processing the file: ${error.message}`);

View File

@ -0,0 +1,260 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { EInvoice } from '../ts/einvoice.js';
import { InvoiceFormat } from '../ts/interfaces/common.js';
import { FormatDetector } from '../ts/formats/utils/format.detector.js';
import { TestFileHelpers, TestFileCategories, InvoiceAssertions, PerformanceUtils } from './test-utils.js';
import * as path from 'path';
/**
* Comprehensive format detection tests using the corpus assets
*/
// Test format detection for CII XML-Rechnung files
tap.test('Format Detection - CII XML-Rechnung files', async () => {
const files = await TestFileHelpers.getTestFiles(TestFileCategories.CII_XMLRECHNUNG, '*.xml');
console.log(`Testing ${files.length} CII XML-Rechnung files`);
for (const file of files) {
const xmlBuffer = await TestFileHelpers.loadTestFile(file);
const xmlString = xmlBuffer.toString('utf-8');
const { result: format, duration } = await PerformanceUtils.measure(
'cii-detection',
async () => FormatDetector.detectFormat(xmlString)
);
// CII files should be detected as either CII or XRechnung
const validFormats = [InvoiceFormat.CII, InvoiceFormat.XRECHNUNG];
expect(validFormats).toContain(format);
console.log(`${path.basename(file)}: ${format} (${duration.toFixed(2)}ms)`);
}
});
// Test format detection for UBL XML-Rechnung files
tap.test('Format Detection - UBL XML-Rechnung files', async () => {
const files = await TestFileHelpers.getTestFiles(TestFileCategories.UBL_XMLRECHNUNG, '*.xml');
console.log(`Testing ${files.length} UBL XML-Rechnung files`);
for (const file of files) {
const xmlBuffer = await TestFileHelpers.loadTestFile(file);
const xmlString = xmlBuffer.toString('utf-8');
const { result: format, duration } = await PerformanceUtils.measure(
'ubl-detection',
async () => FormatDetector.detectFormat(xmlString)
);
// UBL files should be detected as either UBL or XRechnung
const validFormats = [InvoiceFormat.UBL, InvoiceFormat.XRECHNUNG];
expect(validFormats).toContain(format);
console.log(`${path.basename(file)}: ${format} (${duration.toFixed(2)}ms)`);
}
});
// Test format detection for PEPPOL files
tap.test('Format Detection - PEPPOL large invoice samples', async () => {
const files = await TestFileHelpers.getTestFiles(TestFileCategories.PEPPOL, '*.xml');
console.log(`Testing ${files.length} PEPPOL files`);
for (const file of files) {
const xmlBuffer = await TestFileHelpers.loadTestFile(file);
const xmlString = xmlBuffer.toString('utf-8');
const { result: format, duration } = await PerformanceUtils.measure(
'peppol-detection',
async () => FormatDetector.detectFormat(xmlString)
);
// PEPPOL files are typically UBL format
expect(format).toEqual(InvoiceFormat.UBL);
console.log(`${path.basename(file)}: ${format} (${duration.toFixed(2)}ms)`);
}
});
// Test format detection for FatturaPA files
tap.test('Format Detection - FatturaPA Italian invoice format', async () => {
const files = await TestFileHelpers.getTestFiles(TestFileCategories.FATTURAPA, '*.xml');
console.log(`Testing ${files.length} FatturaPA files`);
let detectedCount = 0;
for (const file of files) {
try {
const xmlBuffer = await TestFileHelpers.loadTestFile(file);
const xmlString = xmlBuffer.toString('utf-8');
const { result: format, duration } = await PerformanceUtils.measure(
'fatturapa-detection',
async () => FormatDetector.detectFormat(xmlString)
);
// FatturaPA detection might not be fully implemented yet
if (format === InvoiceFormat.FATTURAPA) {
detectedCount++;
}
console.log(`${format === InvoiceFormat.FATTURAPA ? '✓' : '○'} ${path.basename(file)}: ${format} (${duration.toFixed(2)}ms)`);
} catch (error) {
console.log(`${path.basename(file)}: Error - ${error.message}`);
}
}
// Log if FatturaPA detection needs implementation
if (detectedCount === 0 && files.length > 0) {
console.log('Note: FatturaPA format detection may need implementation');
}
});
// Test format detection for EN16931 examples
tap.test('Format Detection - EN16931 example files', async () => {
// Test CII examples
const ciiFiles = await TestFileHelpers.getTestFiles(TestFileCategories.EN16931_EXAMPLES_CII, '*.xml');
console.log(`Testing ${ciiFiles.length} EN16931 CII examples`);
for (const file of ciiFiles) {
const xmlBuffer = await TestFileHelpers.loadTestFile(file);
const xmlString = xmlBuffer.toString('utf-8');
const format = FormatDetector.detectFormat(xmlString);
expect([InvoiceFormat.CII, InvoiceFormat.FACTURX, InvoiceFormat.XRECHNUNG]).toContain(format);
console.log(`${path.basename(file)}: ${format}`);
}
// Test UBL examples
const ublFiles = await TestFileHelpers.getTestFiles(TestFileCategories.EN16931_EXAMPLES_UBL, '*.xml');
console.log(`Testing ${ublFiles.length} EN16931 UBL examples`);
for (const file of ublFiles) {
const xmlBuffer = await TestFileHelpers.loadTestFile(file);
const xmlString = xmlBuffer.toString('utf-8');
const format = FormatDetector.detectFormat(xmlString);
expect([InvoiceFormat.UBL, InvoiceFormat.XRECHNUNG]).toContain(format);
console.log(`${path.basename(file)}: ${format}`);
}
});
// Test format detection with malformed/edge case files
tap.test('Format Detection - Edge cases and error handling', async () => {
// Test empty XML
const emptyFormat = FormatDetector.detectFormat('');
expect(emptyFormat).toEqual(InvoiceFormat.UNKNOWN);
console.log('✓ Empty string returns UNKNOWN');
// Test non-XML content
const textFormat = FormatDetector.detectFormat('This is not XML');
expect(textFormat).toEqual(InvoiceFormat.UNKNOWN);
console.log('✓ Non-XML text returns UNKNOWN');
// Test minimal XML
const minimalFormat = FormatDetector.detectFormat('<?xml version="1.0"?><root></root>');
expect(minimalFormat).toEqual(InvoiceFormat.UNKNOWN);
console.log('✓ Minimal XML returns UNKNOWN');
// Test with BOM
const bomXml = '\ufeff<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"></Invoice>';
const bomFormat = FormatDetector.detectFormat(bomXml);
expect(bomFormat).toEqual(InvoiceFormat.UBL);
console.log('✓ XML with BOM is handled correctly');
});
// Test format detection performance
tap.test('Format Detection - Performance benchmarks', async () => {
console.log('\nPerformance Benchmarks:');
// Test with small file
const smallXml = '<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>123</ID></Invoice>';
const smallTimes: number[] = [];
for (let i = 0; i < 100; i++) {
const start = performance.now();
FormatDetector.detectFormat(smallXml);
smallTimes.push(performance.now() - start);
}
const avgSmall = smallTimes.reduce((a, b) => a + b) / smallTimes.length;
console.log(`Small XML (${smallXml.length} bytes): avg ${avgSmall.toFixed(3)}ms`);
expect(avgSmall).toBeLessThan(1); // Should be very fast
// Test with large file (if available)
try {
const largeFiles = await TestFileHelpers.getTestFiles(TestFileCategories.PEPPOL, 'Large*.xml');
if (largeFiles.length > 0) {
const largeBuffer = await TestFileHelpers.loadTestFile(largeFiles[0]);
const largeXml = largeBuffer.toString('utf-8');
const largeTimes: number[] = [];
for (let i = 0; i < 10; i++) {
const start = performance.now();
FormatDetector.detectFormat(largeXml);
largeTimes.push(performance.now() - start);
}
const avgLarge = largeTimes.reduce((a, b) => a + b) / largeTimes.length;
console.log(`Large XML (${largeXml.length} bytes): avg ${avgLarge.toFixed(3)}ms`);
expect(avgLarge).toBeLessThan(10); // Should still be reasonably fast
}
} catch (error) {
console.log('Large file test skipped - no large files available');
}
});
// Test format detection from PDF embedded XML
tap.test('Format Detection - ZUGFeRD PDFs with embedded XML', async () => {
const pdfFiles = await TestFileHelpers.getTestFiles(TestFileCategories.ZUGFERD_V2_CORRECT, '*.pdf');
console.log(`Testing ${pdfFiles.length} ZUGFeRD v2 PDF files`);
let successCount = 0;
for (const file of pdfFiles.slice(0, 5)) { // Test first 5 files for speed
try {
const pdfBuffer = await TestFileHelpers.loadTestFile(file);
const einvoice = await EInvoice.fromPdf(pdfBuffer);
const format = einvoice.getFormat();
expect([InvoiceFormat.ZUGFERD, InvoiceFormat.FACTURX]).toContain(format);
successCount++;
console.log(`${path.basename(file)}: ${format}`);
} catch (error) {
console.log(`${path.basename(file)}: PDF extraction not available`);
}
}
if (successCount > 0) {
console.log(`Successfully detected format from ${successCount} PDF files`);
}
});
// Generate performance report
tap.test('Format Detection - Performance Summary', async () => {
const report = PerformanceUtils.generateReport();
console.log('\n' + report);
// Check that detection is generally fast
const ciiStats = PerformanceUtils.getStats('cii-detection');
if (ciiStats) {
expect(ciiStats.avg).toBeLessThan(5); // Average should be under 5ms
console.log(`CII detection average: ${ciiStats.avg.toFixed(2)}ms`);
}
const ublStats = PerformanceUtils.getStats('ubl-detection');
if (ublStats) {
expect(ublStats.avg).toBeLessThan(5); // Average should be under 5ms
console.log(`UBL detection average: ${ublStats.avg.toFixed(2)}ms`);
}
});
// Test the confidence scoring (if implemented)
tap.test('Format Detection - Confidence scoring', async () => {
// This test is for future implementation when confidence scoring is added
console.log('Confidence scoring tests - placeholder for future implementation');
// Example of what we might test:
// const result = FormatDetector.detectFormatWithConfidence(xml);
// expect(result.format).toEqual(InvoiceFormat.UBL);
// expect(result.confidence).toBeGreaterThan(0.8);
});
tap.start();

352
test/test.pdf-operations.ts Normal file
View File

@ -0,0 +1,352 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { EInvoice, EInvoicePDFError } from '../ts/index.js';
import { InvoiceFormat } from '../ts/interfaces/common.js';
import { TestFileHelpers, TestFileCategories, PerformanceUtils, TestInvoiceFactory } from './test-utils.js';
import * as path from 'path';
import { promises as fs } from 'fs';
/**
* Comprehensive PDF operations test suite
*/
// Test PDF extraction from ZUGFeRD v1 files
tap.test('PDF Operations - Extract XML from ZUGFeRD v1 PDFs', async () => {
const pdfFiles = await TestFileHelpers.getTestFiles(TestFileCategories.ZUGFERD_V1_CORRECT, '*.pdf');
console.log(`Testing XML extraction from ${pdfFiles.length} ZUGFeRD v1 PDFs`);
let successCount = 0;
let failCount = 0;
const extractionTimes: number[] = [];
for (const file of pdfFiles.slice(0, 5)) { // Test first 5 for speed
const fileName = path.basename(file);
try {
const pdfBuffer = await TestFileHelpers.loadTestFile(file);
const { result: einvoice, duration } = await PerformanceUtils.measure(
'pdf-extraction-v1',
async () => EInvoice.fromPdf(pdfBuffer)
);
extractionTimes.push(duration);
// Verify extraction succeeded
expect(einvoice).toBeTruthy();
expect(einvoice.getXml()).toBeTruthy();
expect(einvoice.getXml().length).toBeGreaterThan(100);
// Check format detection
const format = einvoice.getFormat();
expect([InvoiceFormat.ZUGFERD, InvoiceFormat.FACTURX]).toContain(format);
successCount++;
console.log(`${fileName}: Extracted ${einvoice.getXml().length} bytes, format: ${format} (${duration.toFixed(2)}ms)`);
// Verify basic invoice data
expect(einvoice.id).toBeTruthy();
expect(einvoice.from.name).toBeTruthy();
expect(einvoice.to.name).toBeTruthy();
} catch (error) {
failCount++;
if (error instanceof EInvoicePDFError) {
console.log(`${fileName}: ${error.message}`);
console.log(` Recovery suggestions: ${error.getRecoverySuggestions().join(', ')}`);
} else {
console.log(`${fileName}: ${error.message}`);
}
}
}
console.log(`\nExtraction Summary: ${successCount} succeeded, ${failCount} failed`);
if (extractionTimes.length > 0) {
const avgTime = extractionTimes.reduce((a, b) => a + b) / extractionTimes.length;
console.log(`Average extraction time: ${avgTime.toFixed(2)}ms`);
}
expect(successCount).toBeGreaterThan(0);
});
// Test PDF extraction from ZUGFeRD v2/Factur-X files
tap.test('PDF Operations - Extract XML from ZUGFeRD v2/Factur-X PDFs', async () => {
const pdfFiles = await TestFileHelpers.getTestFiles(TestFileCategories.ZUGFERD_V2_CORRECT, '*.pdf');
console.log(`Testing XML extraction from ${pdfFiles.length} ZUGFeRD v2/Factur-X PDFs`);
const profileStats: Record<string, number> = {};
for (const file of pdfFiles.slice(0, 10)) { // Test first 10
const fileName = path.basename(file);
try {
const pdfBuffer = await TestFileHelpers.loadTestFile(file);
const einvoice = await EInvoice.fromPdf(pdfBuffer);
// Extract profile from filename if present
const profileMatch = fileName.match(/(BASIC|COMFORT|EXTENDED|MINIMUM|EN16931)/i);
const profile = profileMatch ? profileMatch[1].toUpperCase() : 'UNKNOWN';
profileStats[profile] = (profileStats[profile] || 0) + 1;
console.log(`${fileName}: Profile ${profile}, Format ${einvoice.getFormat()}`);
// Test that we can re-export the invoice
const xml = await einvoice.exportXml('facturx');
expect(xml).toBeTruthy();
expect(xml).toInclude('CrossIndustryInvoice');
} catch (error) {
console.log(`${fileName}: ${error.message}`);
}
}
console.log('\nProfile distribution:', profileStats);
});
// Test PDF embedding (creating PDFs with XML)
tap.test('PDF Operations - Embed XML into PDF', async () => {
// Create a test invoice
const invoice = new EInvoice();
Object.assign(invoice, TestInvoiceFactory.createComplexInvoice());
// Generate XML
const xml = await invoice.exportXml('facturx');
expect(xml).toBeTruthy();
console.log(`Generated XML: ${xml.length} bytes`);
// Create a minimal PDF for testing
const pdfBuffer = await createMinimalTestPDF();
invoice.pdf = {
name: 'test-invoice.pdf',
id: 'test-pdf-001',
metadata: { textExtraction: '' },
buffer: pdfBuffer
};
// Test embedding
try {
const { result: resultPdf, duration } = await PerformanceUtils.measure(
'pdf-embedding',
async () => invoice.exportPdf('facturx')
);
expect(resultPdf).toBeTruthy();
expect(resultPdf.buffer).toBeTruthy();
expect(resultPdf.buffer.length).toBeGreaterThan(pdfBuffer.length);
console.log(`✓ Successfully embedded XML into PDF (${duration.toFixed(2)}ms)`);
console.log(` Original PDF: ${pdfBuffer.length} bytes`);
console.log(` Result PDF: ${resultPdf.buffer.length} bytes`);
console.log(` Size increase: ${resultPdf.buffer.length - pdfBuffer.length} bytes`);
// Verify the embedded XML can be extracted
const verification = await EInvoice.fromPdf(resultPdf.buffer);
expect(verification.getXml()).toBeTruthy();
expect(verification.getFormat()).toEqual(InvoiceFormat.FACTURX);
console.log('✓ Verified: Embedded XML can be extracted successfully');
} catch (error) {
if (error instanceof EInvoicePDFError) {
console.log(`✗ Embedding failed: ${error.message}`);
console.log(` Operation: ${error.operation}`);
console.log(` Suggestions: ${error.getRecoverySuggestions().join(', ')}`);
}
throw error;
}
});
// Test PDF extraction error handling
tap.test('PDF Operations - Error handling for invalid PDFs', async () => {
// Test with empty buffer
try {
await EInvoice.fromPdf(new Uint8Array(0));
expect.fail('Should have thrown an error for empty PDF');
} catch (error) {
expect(error).toBeInstanceOf(EInvoicePDFError);
if (error instanceof EInvoicePDFError) {
expect(error.operation).toEqual('extract');
console.log('✓ Empty PDF error handled correctly');
}
}
// Test with non-PDF data
try {
const textBuffer = Buffer.from('This is not a PDF file');
await EInvoice.fromPdf(textBuffer);
expect.fail('Should have thrown an error for non-PDF data');
} catch (error) {
expect(error).toBeInstanceOf(EInvoicePDFError);
console.log('✓ Non-PDF data error handled correctly');
}
// Test with corrupted PDF header
try {
const corruptPdf = Buffer.from('%PDF-1.4\nCorrupted content');
await EInvoice.fromPdf(corruptPdf);
expect.fail('Should have thrown an error for corrupted PDF');
} catch (error) {
expect(error).toBeInstanceOf(EInvoicePDFError);
console.log('✓ Corrupted PDF error handled correctly');
}
});
// Test failed PDF extractions from corpus
tap.test('PDF Operations - Handle PDFs without XML gracefully', async () => {
const failPdfs = await TestFileHelpers.getTestFiles(TestFileCategories.ZUGFERD_V1_FAIL, '*.pdf');
console.log(`Testing ${failPdfs.length} PDFs expected to fail`);
for (const file of failPdfs) {
const fileName = path.basename(file);
try {
const pdfBuffer = await TestFileHelpers.loadTestFile(file);
await EInvoice.fromPdf(pdfBuffer);
console.log(`${fileName}: Unexpectedly succeeded (might have XML)`);
} catch (error) {
if (error instanceof EInvoicePDFError) {
expect(error.operation).toEqual('extract');
console.log(`${fileName}: Correctly failed - ${error.message}`);
} else {
console.log(`${fileName}: Wrong error type - ${error.message}`);
}
}
}
});
// Test PDF metadata preservation
tap.test('PDF Operations - Metadata preservation during embedding', async () => {
// Load a real PDF from corpus
const pdfFiles = await TestFileHelpers.getTestFiles(TestFileCategories.ZUGFERD_V2_CORRECT, '*.pdf');
if (pdfFiles.length > 0) {
const originalPdfBuffer = await TestFileHelpers.loadTestFile(pdfFiles[0]);
try {
// Extract from original
const originalInvoice = await EInvoice.fromPdf(originalPdfBuffer);
// Re-embed with different format
const reembedded = await originalInvoice.exportPdf('xrechnung');
// Extract again
const reextracted = await EInvoice.fromPdf(reembedded.buffer);
// Compare key fields
expect(reextracted.from.name).toEqual(originalInvoice.from.name);
expect(reextracted.to.name).toEqual(originalInvoice.to.name);
expect(reextracted.items.length).toEqual(originalInvoice.items.length);
console.log('✓ Metadata preserved through re-embedding cycle');
} catch (error) {
console.log(`○ Metadata preservation test skipped: ${error.message}`);
}
}
});
// Test PDF size constraints
tap.test('PDF Operations - Performance with large PDFs', async () => {
const largePdfSize = 10 * 1024 * 1024; // 10MB
const largePdfBuffer = Buffer.alloc(largePdfSize);
// Create a simple PDF header
const pdfHeader = Buffer.from('%PDF-1.4\n');
pdfHeader.copy(largePdfBuffer);
console.log(`Testing with ${(largePdfSize / 1024 / 1024).toFixed(1)}MB PDF`);
const startTime = performance.now();
try {
await EInvoice.fromPdf(largePdfBuffer);
} catch (error) {
// Expected to fail, we're testing performance
const duration = performance.now() - startTime;
console.log(`✓ Large PDF processed in ${duration.toFixed(2)}ms`);
expect(duration).toBeLessThan(5000); // Should fail fast, not hang
}
});
// Test concurrent PDF operations
tap.test('PDF Operations - Concurrent processing', async () => {
const pdfFiles = await TestFileHelpers.getTestFiles(TestFileCategories.ZUGFERD_V2_CORRECT, '*.pdf');
const testFiles = pdfFiles.slice(0, 5);
if (testFiles.length > 0) {
console.log(`Testing concurrent processing of ${testFiles.length} PDFs`);
const startTime = performance.now();
// Process all PDFs concurrently
const promises = testFiles.map(async (file) => {
try {
const pdfBuffer = await TestFileHelpers.loadTestFile(file);
const einvoice = await EInvoice.fromPdf(pdfBuffer);
return { success: true, format: einvoice.getFormat() };
} catch (error) {
return { success: false, error: error.message };
}
});
const results = await Promise.all(promises);
const duration = performance.now() - startTime;
const successCount = results.filter(r => r.success).length;
console.log(`✓ Processed ${successCount}/${testFiles.length} PDFs concurrently in ${duration.toFixed(2)}ms`);
console.log(` Average time per PDF: ${(duration / testFiles.length).toFixed(2)}ms`);
}
});
// Performance summary
tap.test('PDF Operations - Performance Summary', async () => {
const stats = {
extraction: PerformanceUtils.getStats('pdf-extraction-v1'),
embedding: PerformanceUtils.getStats('pdf-embedding')
};
console.log('\nPDF Operations Performance Summary:');
if (stats.extraction) {
console.log('PDF Extraction (ZUGFeRD v1):');
console.log(` Average: ${stats.extraction.avg.toFixed(2)}ms`);
console.log(` Min/Max: ${stats.extraction.min.toFixed(2)}ms / ${stats.extraction.max.toFixed(2)}ms`);
}
if (stats.embedding) {
console.log('PDF Embedding:');
console.log(` Average: ${stats.embedding.avg.toFixed(2)}ms`);
}
// Performance assertions
if (stats.extraction && stats.extraction.count > 3) {
expect(stats.extraction.avg).toBeLessThan(1000); // Should extract in under 1 second on average
}
});
// Helper function to create a minimal test PDF
async function createMinimalTestPDF(): Promise<Uint8Array> {
// This creates a very minimal valid PDF
const pdfContent = `%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << >> >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
217
%%EOF`;
return new Uint8Array(Buffer.from(pdfContent));
}
tap.start();

View File

@ -1,29 +1,29 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { EInvoice } from '../ts/einvoice.js';
import { ValidationLevel, InvoiceFormat } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Test loading and parsing real CII (Factur-X/ZUGFeRD) XML files
tap.test('XInvoice should load and parse real CII XML files', async () => {
tap.test('EInvoice should load and parse real CII XML files', async () => {
// Test with a simple CII file
const xmlPath = path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/CII/EN16931_Einfach.cii.xml');
const xmlContent = await fs.readFile(xmlPath, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(xmlContent);
// Check that the XInvoice instance has the expected properties
expect(xinvoice).toBeTruthy();
expect(xinvoice.from).toBeTruthy();
expect(xinvoice.to).toBeTruthy();
expect(xinvoice.items).toBeArray();
// Check that the EInvoice instance has the expected properties
expect(einvoice).toBeTruthy();
expect(einvoice.from).toBeTruthy();
expect(einvoice.to).toBeTruthy();
expect(einvoice.items).toBeArray();
// Check that the format is detected correctly
expect(xinvoice.getFormat()).toEqual(InvoiceFormat.FACTURX);
expect(einvoice.getFormat()).toEqual(InvoiceFormat.FACTURX);
// Check that the invoice can be exported back to XML
const exportedXml = await xinvoice.exportXml('facturx');
const exportedXml = await einvoice.exportXml('facturx');
expect(exportedXml).toBeTruthy();
expect(exportedXml).toInclude('CrossIndustryInvoice');
@ -34,47 +34,47 @@ tap.test('XInvoice should load and parse real CII XML files', async () => {
});
// Test loading and parsing real UBL (XRechnung) XML files
tap.test('XInvoice should load and parse real UBL XML files', async () => {
tap.test('EInvoice should load and parse real UBL XML files', async () => {
// Test with a simple UBL file
const xmlPath = path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/UBL/EN16931_Einfach.ubl.xml');
const xmlContent = await fs.readFile(xmlPath, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(xmlContent);
// Check that the XInvoice instance has the expected properties
expect(xinvoice).toBeTruthy();
expect(xinvoice.from).toBeTruthy();
expect(xinvoice.to).toBeTruthy();
expect(xinvoice.items).toBeArray();
// Check that the EInvoice instance has the expected properties
expect(einvoice).toBeTruthy();
expect(einvoice.from).toBeTruthy();
expect(einvoice.to).toBeTruthy();
expect(einvoice.items).toBeArray();
// Check that the format is detected correctly
// This file is a UBL format, not XRechnung
expect(xinvoice.getFormat()).toEqual(InvoiceFormat.UBL);
expect(einvoice.getFormat()).toEqual(InvoiceFormat.UBL);
// Skip the export test for now since UBL encoder is not implemented yet
// This is a legitimate limitation of the current implementation
console.log('Skipping UBL export test - UBL encoder not yet implemented');
// Just test that the format was detected correctly
expect(xinvoice.getFormat()).toEqual(InvoiceFormat.UBL);
expect(einvoice.getFormat()).toEqual(InvoiceFormat.UBL);
});
// Test PDF creation and extraction with real XML files
tap.test('XInvoice should create and parse PDFs with embedded XML', async () => {
tap.test('EInvoice should create and parse PDFs with embedded XML', async () => {
// Find a real CII XML file to use
const xmlPath = path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/CII/EN16931_Einfach.cii.xml');
const xmlContent = await fs.readFile(xmlPath, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(xmlContent);
// Check that the XInvoice instance has the expected properties
expect(xinvoice).toBeTruthy();
expect(xinvoice.from).toBeTruthy();
expect(xinvoice.to).toBeTruthy();
expect(xinvoice.items).toBeArray();
// Check that the EInvoice instance has the expected properties
expect(einvoice).toBeTruthy();
expect(einvoice.from).toBeTruthy();
expect(einvoice.to).toBeTruthy();
expect(einvoice.items).toBeArray();
// Create a simple PDF document
const { PDFDocument } = await import('pdf-lib');
@ -84,7 +84,7 @@ tap.test('XInvoice should create and parse PDFs with embedded XML', async () =>
const pdfBytes = await pdfDoc.save();
// Set the PDF buffer
xinvoice.pdf = {
einvoice.pdf = {
name: 'test-invoice.pdf',
id: `test-invoice-${Date.now()}`,
metadata: {
@ -94,7 +94,7 @@ tap.test('XInvoice should create and parse PDFs with embedded XML', async () =>
};
// Export as PDF with embedded XML
const exportedPdf = await xinvoice.exportPdf('facturx');
const exportedPdf = await einvoice.exportPdf('facturx');
expect(exportedPdf).toBeTruthy();
expect(exportedPdf.buffer).toBeTruthy();
@ -104,21 +104,21 @@ tap.test('XInvoice should create and parse PDFs with embedded XML', async () =>
await fs.writeFile(path.join(testDir, 'test-invoice-with-xml.pdf'), exportedPdf.buffer);
// Now try to load the PDF back
const loadedXInvoice = await XInvoice.fromPdf(exportedPdf.buffer);
const loadedEInvoice = await EInvoice.fromPdf(exportedPdf.buffer);
// Check that the loaded XInvoice has the expected properties
expect(loadedXInvoice).toBeTruthy();
expect(loadedXInvoice.from).toBeTruthy();
expect(loadedXInvoice.to).toBeTruthy();
expect(loadedXInvoice.items).toBeArray();
// Check that the loaded EInvoice has the expected properties
expect(loadedEInvoice).toBeTruthy();
expect(loadedEInvoice.from).toBeTruthy();
expect(loadedEInvoice.to).toBeTruthy();
expect(loadedEInvoice.items).toBeArray();
// Check that key properties are present
expect(loadedXInvoice.id).toBeTruthy();
expect(loadedXInvoice.from.name).toBeTruthy();
expect(loadedXInvoice.to.name).toBeTruthy();
expect(loadedEInvoice.id).toBeTruthy();
expect(loadedEInvoice.from.name).toBeTruthy();
expect(loadedEInvoice.to.name).toBeTruthy();
// Export the loaded invoice back to XML
const reExportedXml = await loadedXInvoice.exportXml('facturx');
const reExportedXml = await loadedEInvoice.exportXml('facturx');
expect(reExportedXml).toBeTruthy();
expect(reExportedXml).toInclude('CrossIndustryInvoice');
@ -153,16 +153,16 @@ async function findPdfFiles(dir: string): Promise<string[]> {
};
// Test validation of real invoice files
tap.test('XInvoice should validate real invoice files', async () => {
tap.test('EInvoice should validate real invoice files', async () => {
// Test with a simple CII file
const xmlPath = path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/CII/EN16931_Einfach.cii.xml');
const xmlContent = await fs.readFile(xmlPath, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(xmlContent);
// Validate the XML
const result = await xinvoice.validate(ValidationLevel.SYNTAX);
const result = await einvoice.validate(ValidationLevel.SYNTAX);
// Check that validation passed
expect(result.valid).toBeTrue();
@ -170,7 +170,7 @@ tap.test('XInvoice should validate real invoice files', async () => {
});
// Test with multiple real invoice files
tap.test('XInvoice should handle multiple real invoice files', async () => {
tap.test('EInvoice should handle multiple real invoice files', async () => {
// Get all CII files
const ciiDir = path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/CII');
const ciiFiles = await fs.readdir(ciiDir);
@ -184,19 +184,19 @@ tap.test('XInvoice should handle multiple real invoice files', async () => {
const xmlPath = path.join(ciiDir, file);
const xmlContent = await fs.readFile(xmlPath, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(xmlContent);
// Check that the XInvoice instance has the expected properties
expect(xinvoice).toBeTruthy();
expect(xinvoice.from).toBeTruthy();
expect(xinvoice.to).toBeTruthy();
// Check that the EInvoice instance has the expected properties
expect(einvoice).toBeTruthy();
expect(einvoice.from).toBeTruthy();
expect(einvoice.to).toBeTruthy();
// Check that the format is detected correctly
expect(xinvoice.getFormat()).toEqual(InvoiceFormat.FACTURX);
expect(einvoice.getFormat()).toEqual(InvoiceFormat.FACTURX);
// Check that the invoice can be exported back to XML
const exportedXml = await xinvoice.exportXml('facturx');
const exportedXml = await einvoice.exportXml('facturx');
expect(exportedXml).toBeTruthy();
expect(exportedXml).toInclude('CrossIndustryInvoice');
}

View File

@ -1,11 +1,11 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { EInvoice } from '../ts/einvoice.js';
import { InvoiceFormat } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Test a simple subset of corpus files
tap.test('XInvoice should handle a simple subset of corpus files', async () => {
tap.test('EInvoice should handle a simple subset of corpus files', async () => {
// Test a few specific files that we know work
const testFiles = [
// CII files
@ -32,25 +32,25 @@ tap.test('XInvoice should handle a simple subset of corpus files', async () => {
// Read the file
const xmlContent = await fs.readFile(file, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(xmlContent);
// Check that the XInvoice instance has the expected properties
if (xinvoice && xinvoice.from && xinvoice.to && xinvoice.items) {
// Check that the EInvoice instance has the expected properties
if (einvoice && einvoice.from && einvoice.to && einvoice.items) {
console.log('✅ Success: File loaded and parsed successfully');
console.log(`Format: ${xinvoice.getFormat()}`);
console.log(`From: ${xinvoice.from.name}`);
console.log(`To: ${xinvoice.to.name}`);
console.log(`Items: ${xinvoice.items.length}`);
console.log(`Format: ${einvoice.getFormat()}`);
console.log(`From: ${einvoice.from.name}`);
console.log(`To: ${einvoice.to.name}`);
console.log(`Items: ${einvoice.items.length}`);
// Try to export the invoice back to XML
try {
let exportFormat = 'facturx';
if (xinvoice.getFormat() === InvoiceFormat.UBL || xinvoice.getFormat() === InvoiceFormat.XRECHNUNG) {
if (einvoice.getFormat() === InvoiceFormat.UBL || einvoice.getFormat() === InvoiceFormat.XRECHNUNG) {
exportFormat = 'xrechnung';
}
const exportedXml = await xinvoice.exportXml(exportFormat as any);
const exportedXml = await einvoice.exportXml(exportFormat as any);
if (exportedXml) {
console.log('✅ Successfully exported back to XML');

View File

@ -1,10 +1,10 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { EInvoice } from '../ts/einvoice.js';
import { InvoiceFormat, ValidationLevel } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
tap.test('XInvoice should validate corpus files correctly', async () => {
tap.test('EInvoice should validate corpus files correctly', async () => {
// Find test files
const testDir = path.join(process.cwd(), 'test', 'assets');
@ -81,21 +81,21 @@ async function testValidation(files: string[], expectValid: boolean) {
// Load the XML file
const xmlContent = await fs.readFile(file, 'utf8');
// Create an XInvoice instance
let xinvoice: XInvoice;
// Create an EInvoice instance
let einvoice: EInvoice;
// If the file is a PDF, load it as a PDF
if (file.endsWith('.pdf')) {
const pdfBuffer = await fs.readFile(file);
xinvoice = await XInvoice.fromPdf(pdfBuffer);
einvoice = await EInvoice.fromPdf(pdfBuffer);
} else {
// Otherwise, load it as XML
xinvoice = await XInvoice.fromXml(xmlContent);
einvoice = await EInvoice.fromXml(xmlContent);
}
try {
// Validate the invoice
const validationResult = await xinvoice.validate(ValidationLevel.SYNTAX);
const validationResult = await einvoice.validate(ValidationLevel.SYNTAX);
// Check if the validation result matches our expectation
if (validationResult.valid === expectValid) {

View File

@ -0,0 +1,389 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { EInvoice, EInvoiceValidationError } from '../ts/index.js';
import { ValidationLevel, InvoiceFormat } from '../ts/interfaces/common.js';
import { TestFileHelpers, TestFileCategories, InvoiceAssertions, PerformanceUtils } from './test-utils.js';
import * as plugins from '../ts/plugins.js';
/**
* Comprehensive validation test suite using EN16931 test cases
*/
// Test Business Rule validations from EN16931
tap.test('Validation Suite - EN16931 Business Rules (BR-*)', async () => {
const testFiles = await TestFileHelpers.getTestFiles(TestFileCategories.EN16931_UBL_INVOICE, 'BR-*.xml');
console.log(`Testing ${testFiles.length} Business Rule validation files`);
const results = {
passed: 0,
failed: 0,
errors: [] as string[]
};
for (const file of testFiles) {
const fileName = plugins.path.basename(file);
const shouldFail = fileName.includes('BR-'); // These files test specific BR violations
try {
const xmlBuffer = await TestFileHelpers.loadTestFile(file);
const xmlString = xmlBuffer.toString('utf-8');
const einvoice = await EInvoice.fromXml(xmlString);
const { result: validation, duration } = await PerformanceUtils.measure(
'br-validation',
async () => einvoice.validate(ValidationLevel.BUSINESS)
);
// Most BR-*.xml files are designed to fail specific business rules
if (shouldFail && !validation.valid) {
results.passed++;
console.log(`${fileName}: Correctly failed validation (${duration.toFixed(2)}ms)`);
// Check that the correct BR code is in the errors
const brCode = fileName.match(/BR-\d+/)?.[0];
if (brCode) {
const hasCorrectError = validation.errors.some(e => e.code.includes(brCode));
if (!hasCorrectError) {
console.log(` ⚠ Expected error code ${brCode} not found in: ${validation.errors.map(e => e.code).join(', ')}`);
}
}
} else if (!shouldFail && validation.valid) {
results.passed++;
console.log(`${fileName}: Correctly passed validation (${duration.toFixed(2)}ms)`);
} else {
results.failed++;
results.errors.push(`${fileName}: Unexpected result - valid: ${validation.valid}`);
console.log(`${fileName}: Unexpected validation result`);
if (validation.errors.length > 0) {
console.log(` Errors: ${validation.errors.map(e => `${e.code}: ${e.message}`).join('; ')}`);
}
}
} catch (error) {
results.failed++;
results.errors.push(`${fileName}: ${error.message}`);
console.log(`${fileName}: Error - ${error.message}`);
}
}
console.log(`\nBusiness Rules Summary: ${results.passed} passed, ${results.failed} failed`);
if (results.errors.length > 0) {
console.log('Failures:', results.errors);
}
// Allow some failures as not all validators may be implemented
expect(results.passed).toBeGreaterThan(0);
});
// Test Codelist validations
tap.test('Validation Suite - EN16931 Codelist validations (BR-CL-*)', async () => {
const testFiles = await TestFileHelpers.getTestFiles(TestFileCategories.EN16931_UBL_INVOICE, 'BR-CL-*.xml');
console.log(`Testing ${testFiles.length} Codelist validation files`);
let validatedCount = 0;
for (const file of testFiles.slice(0, 10)) { // Test first 10 for speed
const fileName = plugins.path.basename(file);
try {
const xmlBuffer = await TestFileHelpers.loadTestFile(file);
const xmlString = xmlBuffer.toString('utf-8');
const einvoice = await EInvoice.fromXml(xmlString);
const validation = await einvoice.validate(ValidationLevel.SEMANTIC);
validatedCount++;
// These files test invalid code values
if (!validation.valid) {
const clCode = fileName.match(/BR-CL-\d+/)?.[0];
console.log(`${fileName}: Detected invalid code (${clCode})`);
} else {
console.log(`${fileName}: Validation passed (may need stricter codelist checking)`);
}
} catch (error) {
console.log(`${fileName}: Error - ${error.message}`);
}
}
expect(validatedCount).toBeGreaterThan(0);
console.log(`Validated ${validatedCount} codelist test files`);
});
// Test syntax validation
tap.test('Validation Suite - Syntax validation levels', async () => {
const xmlWithError = `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>123</ID>
<IssueDate>not-a-date</IssueDate>
<InvalidElement>This element doesn't belong here</InvalidElement>
</Invoice>`;
const einvoice = new EInvoice();
// Test that we can catch parsing errors
try {
await einvoice.loadXml(xmlWithError);
// Syntax validation should catch schema violations
const syntaxValidation = await einvoice.validate(ValidationLevel.SYNTAX);
console.log('Syntax validation:', syntaxValidation.valid ? 'PASSED' : 'FAILED');
if (!syntaxValidation.valid) {
console.log('Syntax errors found:', syntaxValidation.errors.length);
syntaxValidation.errors.forEach(err => {
console.log(` - ${err.code}: ${err.message}`);
});
}
} catch (error) {
if (error instanceof EInvoiceValidationError) {
console.log('✓ Validation error caught correctly');
console.log(error.getValidationReport());
}
}
});
// Test validation error reporting
tap.test('Validation Suite - Error reporting and recovery', async () => {
const testInvoice = new EInvoice();
// Try to validate without loading XML
try {
await testInvoice.validate();
} catch (error) {
expect(error).toBeInstanceOf(EInvoiceValidationError);
if (error instanceof EInvoiceValidationError) {
expect(error.validationErrors).toHaveLength(1);
expect(error.validationErrors[0].code).toEqual('VAL-001');
console.log('✓ Empty invoice validation error handled correctly');
}
}
// Test with minimal valid invoice
testInvoice.id = 'TEST-001';
testInvoice.invoiceId = 'INV-001';
testInvoice.from.name = 'Test Seller';
testInvoice.to.name = 'Test Buyer';
testInvoice.items = [{
position: 1,
name: 'Test Item',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
// This should fail because we don't have XML loaded
try {
await testInvoice.validate();
} catch (error) {
expect(error).toBeInstanceOf(EInvoiceValidationError);
console.log('✓ Validation requires loaded XML');
}
});
// Test format-specific validation
tap.test('Validation Suite - Format-specific validation rules', async () => {
// Test XRechnung specific validation
const xrechnungFiles = await TestFileHelpers.getTestFiles(
TestFileCategories.CII_XMLRECHNUNG,
'XRECHNUNG_*.xml'
);
if (xrechnungFiles.length > 0) {
console.log(`Testing ${xrechnungFiles.length} XRechnung files`);
for (const file of xrechnungFiles.slice(0, 3)) {
const xmlBuffer = await TestFileHelpers.loadTestFile(file);
const einvoice = await EInvoice.fromXml(xmlBuffer.toString('utf-8'));
const validation = await einvoice.validate(ValidationLevel.BUSINESS);
console.log(`${plugins.path.basename(file)}: ${validation.valid ? 'VALID' : 'INVALID'}`);
if (!validation.valid && validation.errors.length > 0) {
console.log(` First error: ${validation.errors[0].code} - ${validation.errors[0].message}`);
}
}
}
// Test ZUGFeRD specific validation
console.log('\nTesting ZUGFeRD profile validation:');
const zugferdPdfs = await TestFileHelpers.getTestFiles(
TestFileCategories.ZUGFERD_V2_CORRECT,
'*.pdf'
);
for (const file of zugferdPdfs.slice(0, 2)) {
try {
const pdfBuffer = await TestFileHelpers.loadTestFile(file);
const einvoice = await EInvoice.fromPdf(pdfBuffer);
// Check which ZUGFeRD profile is used
const format = einvoice.getFormat();
console.log(`${plugins.path.basename(file)}: Format ${format}`);
// Validate according to profile
const validation = await einvoice.validate(ValidationLevel.SEMANTIC);
console.log(` Validation: ${validation.valid ? 'VALID' : 'INVALID'}`);
} catch (error) {
console.log(`${plugins.path.basename(file)}: Skipped - ${error.message}`);
}
}
});
// Test validation performance
tap.test('Validation Suite - Performance benchmarks', async () => {
const files = await TestFileHelpers.getTestFiles(
TestFileCategories.UBL_XMLRECHNUNG,
'*.xml'
);
if (files.length > 0) {
const xmlBuffer = await TestFileHelpers.loadTestFile(files[0]);
const xmlString = xmlBuffer.toString('utf-8');
const einvoice = await EInvoice.fromXml(xmlString);
// Benchmark different validation levels
console.log('\nValidation Performance:');
// Syntax validation
const syntaxTimes: number[] = [];
for (let i = 0; i < 10; i++) {
const { duration } = await PerformanceUtils.measure(
'syntax-validation',
async () => einvoice.validate(ValidationLevel.SYNTAX)
);
syntaxTimes.push(duration);
}
const avgSyntax = syntaxTimes.reduce((a, b) => a + b) / syntaxTimes.length;
console.log(`Syntax validation: avg ${avgSyntax.toFixed(2)}ms`);
// Semantic validation
const semanticTimes: number[] = [];
for (let i = 0; i < 10; i++) {
const { duration } = await PerformanceUtils.measure(
'semantic-validation',
async () => einvoice.validate(ValidationLevel.SEMANTIC)
);
semanticTimes.push(duration);
}
const avgSemantic = semanticTimes.reduce((a, b) => a + b) / semanticTimes.length;
console.log(`Semantic validation: avg ${avgSemantic.toFixed(2)}ms`);
// Business validation
const businessTimes: number[] = [];
for (let i = 0; i < 10; i++) {
const { duration } = await PerformanceUtils.measure(
'business-validation',
async () => einvoice.validate(ValidationLevel.BUSINESS)
);
businessTimes.push(duration);
}
const avgBusiness = businessTimes.reduce((a, b) => a + b) / businessTimes.length;
console.log(`Business validation: avg ${avgBusiness.toFixed(2)}ms`);
// Validation should get progressively slower with higher levels
expect(avgSyntax).toBeLessThan(avgSemantic);
expect(avgSemantic).toBeLessThan(avgBusiness);
}
});
// Test calculation validations
tap.test('Validation Suite - Calculation and sum validations', async () => {
const einvoice = new EInvoice();
einvoice.id = 'CALC-TEST-001';
einvoice.invoiceId = 'CALC-001';
einvoice.from.name = 'Calculator Corp';
einvoice.to.name = 'Number Inc';
// Add items with specific calculations
einvoice.items = [
{
position: 1,
name: 'Product A',
unitQuantity: 5,
unitNetPrice: 100, // Total: 500
vatPercentage: 19 // VAT: 95
},
{
position: 2,
name: 'Product B',
unitQuantity: 3,
unitNetPrice: 50, // Total: 150
vatPercentage: 19 // VAT: 28.50
}
];
// Expected totals:
// Net: 650
// VAT: 123.50
// Gross: 773.50
// Generate XML and validate
try {
const xml = await einvoice.exportXml('facturx');
await einvoice.loadXml(xml);
const validation = await einvoice.validate(ValidationLevel.BUSINESS);
if (!validation.valid) {
const calcErrors = validation.errors.filter(e =>
e.code.includes('BR-CO') || e.message.toLowerCase().includes('calc')
);
if (calcErrors.length > 0) {
console.log('Calculation validation errors found:');
calcErrors.forEach(err => {
console.log(` - ${err.code}: ${err.message}`);
});
}
} else {
console.log('✓ Invoice calculations validated successfully');
}
} catch (error) {
console.log(`Calculation validation test skipped: ${error.message}`);
}
});
// Test validation caching
tap.test('Validation Suite - Validation result caching', async () => {
const xmlBuffer = await TestFileHelpers.loadTestFile(
`${TestFileCategories.UBL_XMLRECHNUNG}/EN16931_Einfach.ubl.xml`
);
const einvoice = await EInvoice.fromXml(xmlBuffer.toString('utf-8'));
// First validation (cold)
const { duration: coldDuration } = await PerformanceUtils.measure(
'validation-cold',
async () => einvoice.validate(ValidationLevel.BUSINESS)
);
// Second validation (potentially cached)
const { duration: warmDuration } = await PerformanceUtils.measure(
'validation-warm',
async () => einvoice.validate(ValidationLevel.BUSINESS)
);
console.log(`Cold validation: ${coldDuration.toFixed(2)}ms`);
console.log(`Warm validation: ${warmDuration.toFixed(2)}ms`);
// Check if errors are consistent
const errors1 = einvoice.getValidationErrors();
const errors2 = einvoice.getValidationErrors();
expect(errors1).toEqual(errors2);
});
// Generate validation summary
tap.test('Validation Suite - Summary Report', async () => {
const stats = PerformanceUtils.getStats('br-validation');
if (stats) {
console.log('\nBusiness Rule Validation Performance:');
console.log(` Total validations: ${stats.count}`);
console.log(` Average time: ${stats.avg.toFixed(2)}ms`);
console.log(` Min/Max: ${stats.min.toFixed(2)}ms / ${stats.max.toFixed(2)}ms`);
}
// Test that validation is generally performant
if (stats && stats.count > 10) {
expect(stats.avg).toBeLessThan(100); // Should validate in under 100ms on average
}
});
tap.start();

View File

@ -1,11 +1,11 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { EInvoice } from '../ts/einvoice.js';
import { InvoiceFormat, ValidationLevel } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Test XML-Rechnung corpus (CII and UBL)
tap.test('XInvoice should handle XML-Rechnung corpus', async () => {
tap.test('EInvoice should handle XML-Rechnung corpus', async () => {
// Get all XML-Rechnung files
const ciiFiles = await findFiles(path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/CII'), '.xml');
const ublFiles = await findFiles(path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/UBL'), '.xml');
@ -73,13 +73,13 @@ async function testFiles(files: string[], expectedFormat: InvoiceFormat): Promis
// Read the file
const xmlContent = await fs.readFile(file, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Create EInvoice from XML
const einvoice = await EInvoice.fromXml(xmlContent);
// Check that the XInvoice instance has the expected properties
if (xinvoice && xinvoice.from && xinvoice.to && xinvoice.items) {
// Check that the EInvoice instance has the expected properties
if (einvoice && einvoice.from && einvoice.to && einvoice.items) {
// Check that the format is detected correctly
const format = xinvoice.getFormat();
const format = einvoice.getFormat();
const isCorrectFormat = format === expectedFormat ||
(expectedFormat === InvoiceFormat.CII && format === InvoiceFormat.FACTURX) ||
(expectedFormat === InvoiceFormat.FACTURX && format === InvoiceFormat.CII) ||
@ -94,7 +94,7 @@ async function testFiles(files: string[], expectedFormat: InvoiceFormat): Promis
exportFormat = 'xrechnung';
}
const exportedXml = await xinvoice.exportXml(exportFormat as any);
const exportedXml = await einvoice.exportXml(exportFormat as any);
if (exportedXml) {
// Success

View File

@ -1,11 +1,11 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { EInvoice } from '../ts/einvoice.js';
import { InvoiceFormat, ValidationLevel } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Test ZUGFeRD v1 and v2 corpus
tap.test('XInvoice should handle ZUGFeRD v1 and v2 corpus', async () => {
tap.test('EInvoice should handle ZUGFeRD v1 and v2 corpus', async () => {
// Get all ZUGFeRD files
const zugferdV1CorrectFiles = await findFiles(path.join(process.cwd(), 'test/assets/corpus/ZUGFeRDv1/correct'), '.pdf');
const zugferdV1FailFiles = await findFiles(path.join(process.cwd(), 'test/assets/corpus/ZUGFeRDv1/fail'), '.pdf');
@ -80,19 +80,19 @@ async function testFiles(files: string[], expectSuccess: boolean): Promise<{ suc
// Read the file
const fileBuffer = await fs.readFile(file);
// Create XInvoice from PDF
const xinvoice = await XInvoice.fromPdf(fileBuffer);
// Create EInvoice from PDF
const einvoice = await EInvoice.fromPdf(fileBuffer);
// Check that the XInvoice instance has the expected properties
if (xinvoice && xinvoice.from && xinvoice.to && xinvoice.items) {
// Check that the EInvoice instance has the expected properties
if (einvoice && einvoice.from && einvoice.to && einvoice.items) {
// Check that the format is detected correctly
const format = xinvoice.getFormat();
const format = einvoice.getFormat();
const isZugferd = [InvoiceFormat.ZUGFERD, InvoiceFormat.FACTURX, InvoiceFormat.CII].includes(format);
if (isZugferd) {
// Try to export the invoice to XML
try {
const exportedXml = await xinvoice.exportXml('facturx');
const exportedXml = await einvoice.exportXml('facturx');
if (exportedXml && exportedXml.includes('CrossIndustryInvoice')) {
// Success