/** * Main integrated validator combining all validation capabilities * Orchestrates TypeScript validators, Schematron, and profile-specific rules */ import { IntegratedValidator } from './schematron.integration.js'; import { XRechnungValidator } from './xrechnung.validator.js'; import { PeppolValidator } from './peppol.validator.js'; import { FacturXValidator } from './facturx.validator.js'; import { EN16931BusinessRulesValidator } from './en16931.business-rules.validator.js'; import { CodeListValidator } from './codelist.validator.js'; import type { ValidationResult, ValidationOptions, ValidationReport } from './validation.types.js'; import type { EInvoice } from '../../einvoice.js'; /** * Main validator that combines all validation capabilities */ export class MainValidator { private integratedValidator: IntegratedValidator; private xrechnungValidator: XRechnungValidator; private peppolValidator: PeppolValidator; private facturxValidator: FacturXValidator; private businessRulesValidator: EN16931BusinessRulesValidator; private codeListValidator: CodeListValidator; private schematronEnabled: boolean = false; constructor() { this.integratedValidator = new IntegratedValidator(); this.xrechnungValidator = XRechnungValidator.create(); this.peppolValidator = PeppolValidator.create(); this.facturxValidator = FacturXValidator.create(); this.businessRulesValidator = new EN16931BusinessRulesValidator(); this.codeListValidator = new CodeListValidator(); } /** * Initialize Schematron validation for better coverage */ public async initializeSchematron( profile?: 'EN16931' | 'PEPPOL' | 'XRECHNUNG' ): Promise { try { // Check available Schematron files const available = await this.integratedValidator.getAvailableSchematron(); if (available.length === 0) { console.warn('No Schematron files available. Run: npm run download-schematron'); return; } // Load appropriate Schematron based on profile const standard = profile || 'EN16931'; const format = 'UBL'; // Default to UBL, can be made configurable await this.integratedValidator.loadSchematron( standard === 'XRECHNUNG' ? 'EN16931' : standard, // XRechnung uses EN16931 as base format ); this.schematronEnabled = true; console.log(`Schematron validation enabled for ${standard} ${format}`); } catch (error) { console.warn(`Failed to initialize Schematron: ${error.message}`); } } /** * Validate an invoice with all available validators */ public async validate( invoice: EInvoice, xmlContent?: string, options: ValidationOptions = {} ): Promise { const startTime = Date.now(); const results: ValidationResult[] = []; // Detect profile from invoice const profile = this.detectProfile(invoice); const mergedOptions: ValidationOptions = { ...options, profile: profile as ValidationOptions['profile'] }; // Run base validators if (options.checkCodeLists !== false) { results.push(...this.codeListValidator.validate(invoice)); } results.push(...this.businessRulesValidator.validate(invoice, mergedOptions)); // Run XRechnung-specific validation if applicable if (this.isXRechnungInvoice(invoice)) { const xrResults = this.xrechnungValidator.validateXRechnung(invoice); results.push(...xrResults); } // Run PEPPOL-specific validation if applicable if (this.isPeppolInvoice(invoice)) { const peppolResults = this.peppolValidator.validatePeppol(invoice); results.push(...peppolResults); } // Run Factur-X specific validation if applicable if (this.isFacturXInvoice(invoice)) { const facturxResults = this.facturxValidator.validateFacturX(invoice); results.push(...facturxResults); } // Run Schematron validation if available and XML is provided if (this.schematronEnabled && xmlContent) { try { const schematronReport = await this.integratedValidator.validate( invoice, xmlContent, mergedOptions ); // Extract only Schematron-specific results to avoid duplication const schematronResults = schematronReport.results.filter( r => r.source === 'SCHEMATRON' ); results.push(...schematronResults); } catch (error) { console.warn(`Schematron validation error: ${error.message}`); } } // Remove duplicates (same rule + same field) const uniqueResults = this.deduplicateResults(results); // Calculate statistics const errorCount = uniqueResults.filter(r => r.severity === 'error').length; const warningCount = uniqueResults.filter(r => r.severity === 'warning').length; const infoCount = uniqueResults.filter(r => r.severity === 'info').length; // Estimate coverage const totalRules = this.estimateTotalRules(profile); const rulesChecked = new Set(uniqueResults.map(r => r.ruleId)).size; const coverage = totalRules > 0 ? (rulesChecked / totalRules) * 100 : 0; return { valid: errorCount === 0, profile: profile || 'EN16931', timestamp: new Date().toISOString(), validatorVersion: '2.0.0', rulesetVersion: '1.3.14', results: uniqueResults, errorCount, warningCount, infoCount, rulesChecked, rulesTotal: totalRules, coverage, validationTime: Date.now() - startTime, documentId: invoice.accountingDocId, documentType: invoice.accountingDocType, format: this.detectFormat(xmlContent) } as ValidationReport & { schematronEnabled: boolean }; } /** * Detect profile from invoice metadata */ private detectProfile(invoice: EInvoice): string { const profileId = invoice.metadata?.profileId || ''; const customizationId = invoice.metadata?.customizationId || ''; if (profileId.includes('xrechnung') || customizationId.includes('xrechnung')) { return 'XRECHNUNG_3.0'; } if (profileId.includes('peppol') || customizationId.includes('peppol') || profileId.includes('urn:fdc:peppol.eu')) { return 'PEPPOL_BIS_3.0'; } if (profileId.includes('facturx') || customizationId.includes('facturx') || profileId.includes('zugferd')) { // Try to detect specific Factur-X profile const facturxProfile = this.facturxValidator.detectProfile(invoice); if (facturxProfile) { return `FACTURX_${facturxProfile}`; } return 'FACTURX_EN16931'; } return 'EN16931'; } /** * Check if invoice is XRechnung */ private isXRechnungInvoice(invoice: EInvoice): boolean { const profileId = invoice.metadata?.profileId || ''; const customizationId = invoice.metadata?.customizationId || ''; const xrechnungProfiles = [ 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung', 'urn:cen.eu:en16931:2017#conformant#urn:xeinkauf.de:kosit:xrechnung', 'xrechnung' ]; return xrechnungProfiles.some(profile => profileId.toLowerCase().includes(profile.toLowerCase()) || customizationId.toLowerCase().includes(profile.toLowerCase()) ); } /** * Check if invoice is PEPPOL */ private isPeppolInvoice(invoice: EInvoice): boolean { const profileId = invoice.metadata?.profileId || ''; const customizationId = invoice.metadata?.customizationId || ''; const peppolProfiles = [ 'urn:fdc:peppol.eu:2017:poacc:billing:3.0', 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0', 'peppol-bis-3', 'peppol' ]; return peppolProfiles.some(profile => profileId.toLowerCase().includes(profile.toLowerCase()) || customizationId.toLowerCase().includes(profile.toLowerCase()) ); } /** * Check if invoice is Factur-X */ private isFacturXInvoice(invoice: EInvoice): boolean { const profileId = invoice.metadata?.profileId || ''; const customizationId = invoice.metadata?.customizationId || ''; const format = invoice.metadata?.format; return format?.includes('facturx') || profileId.toLowerCase().includes('facturx') || customizationId.toLowerCase().includes('facturx') || profileId.toLowerCase().includes('zugferd') || customizationId.toLowerCase().includes('zugferd'); } /** * Detect format from XML content */ private detectFormat(xmlContent?: string): 'UBL' | 'CII' | undefined { if (!xmlContent) return undefined; if (xmlContent.includes('urn:oasis:names:specification:ubl:schema:xsd:Invoice-2')) { return 'UBL'; } else if (xmlContent.includes('urn:un:unece:uncefact:data:standard:CrossIndustryInvoice')) { return 'CII'; } return undefined; } /** * Remove duplicate validation results */ private deduplicateResults(results: ValidationResult[]): ValidationResult[] { const seen = new Set(); const unique: ValidationResult[] = []; for (const result of results) { const key = `${result.ruleId}|${result.field || ''}|${result.message}`; if (!seen.has(key)) { seen.add(key); unique.push(result); } } return unique; } /** * Estimate total rules for coverage calculation */ private estimateTotalRules(profile?: string): number { const ruleCounts: Record = { EN16931: 150, 'PEPPOL_BIS_3.0': 250, 'XRECHNUNG_3.0': 280, FACTURX_BASIC: 100, FACTURX_EN16931: 150 }; return ruleCounts[profile || 'EN16931'] || 150; } /** * Validate with automatic format and profile detection */ public async validateAuto( invoice: EInvoice, xmlContent?: string ): Promise { // Auto-detect profile const profile = this.detectProfile(invoice); // Initialize Schematron if not already done if (!this.schematronEnabled && xmlContent) { await this.initializeSchematron( profile.startsWith('XRECHNUNG') ? 'XRECHNUNG' : profile.startsWith('PEPPOL') ? 'PEPPOL' : 'EN16931' ); } return this.validate(invoice, xmlContent, { checkCalculations: true, checkVAT: true, checkCodeLists: true, strictMode: profile.includes('XRECHNUNG') // Strict for XRechnung }); } /** * Get validation capabilities */ public getCapabilities(): { schematron: boolean; xrechnung: boolean; peppol: boolean; facturx: boolean; calculations: boolean; codeLists: boolean; } { return { schematron: this.schematronEnabled, xrechnung: true, peppol: true, facturx: true, calculations: true, codeLists: true }; } /** * Format validation report as text */ public formatReport(report: ValidationReport): string { const lines: string[] = []; lines.push('=== Validation Report ==='); lines.push(`Profile: ${report.profile}`); lines.push(`Valid: ${report.valid ? '✅' : '❌'}`); lines.push(`Timestamp: ${report.timestamp}`); lines.push(''); if (report.errorCount > 0) { lines.push(`Errors: ${report.errorCount}`); report.results .filter(r => r.severity === 'error') .forEach(r => { lines.push(` ❌ [${r.ruleId}] ${r.message}`); if (r.field) lines.push(` Field: ${r.field}`); }); lines.push(''); } if (report.warningCount > 0) { lines.push(`Warnings: ${report.warningCount}`); report.results .filter(r => r.severity === 'warning') .forEach(r => { lines.push(` ⚠️ [${r.ruleId}] ${r.message}`); if (r.field) lines.push(` Field: ${r.field}`); }); lines.push(''); } lines.push('Statistics:'); lines.push(` Rules checked: ${report.rulesChecked}/${report.rulesTotal}`); lines.push(` Coverage: ${report.coverage.toFixed(1)}%`); lines.push(` Validation time: ${report.validationTime}ms`); if ((report as any).schematronEnabled) { lines.push(' Schematron: ✅ Enabled'); } return lines.join('\n'); } } /** * Create a pre-configured validator instance */ export async function createValidator( options: { profile?: 'EN16931' | 'PEPPOL' | 'XRECHNUNG'; enableSchematron?: boolean; } = {} ): Promise { const validator = new MainValidator(); if (options.enableSchematron !== false) { await validator.initializeSchematron(options.profile); } return validator; } // Export for convenience export type { ValidationReport, ValidationResult, ValidationOptions } from './validation.types.js';