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'; import { CorpusLoader } from './corpus.loader.js'; import { PerformanceTracker } from './performance.tracker.js'; // Re-export helpers for convenience export { CorpusLoader, PerformanceTracker }; /** * 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 { return { id: 'TEST-' + Date.now(), accountingDocId: 'INV-TEST-001', accountingDocType: 'invoice', type: 'accounting-doc', date: Date.now(), accountingDocStatus: '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 { 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 { 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 { const fullPath = path.join(process.cwd(), filePath); return fs.readFile(fullPath); } /** * Gets corpus statistics */ static async getCorpusStats(): Promise<{ totalFiles: number; byFormat: Record; byCategory: Record; }> { const stats = { totalFiles: 0, byFormat: {} as Record, byCategory: {} as Record }; 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(); /** * Measures execution time of an async function */ static async measure( name: string, fn: () => Promise ): 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; } }