/** * Integration of official Schematron validation with the EInvoice module */ import { SchematronValidator, HybridValidator } from './schematron.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'; import * as path from 'path'; import { promises as fs } from 'fs'; /** * Integrated validator combining TypeScript and Schematron validation */ export class IntegratedValidator { private hybridValidator: HybridValidator; private schematronValidator: SchematronValidator; private businessRulesValidator: EN16931BusinessRulesValidator; private codeListValidator: CodeListValidator; private schematronLoaded: boolean = false; constructor() { this.schematronValidator = new SchematronValidator(); this.hybridValidator = new HybridValidator(this.schematronValidator); this.businessRulesValidator = new EN16931BusinessRulesValidator(); this.codeListValidator = new CodeListValidator(); // Add TypeScript validators to hybrid pipeline this.setupTypeScriptValidators(); } /** * Setup TypeScript validators in the hybrid pipeline */ private setupTypeScriptValidators(): void { // Wrap business rules validator this.hybridValidator.addTSValidator({ validate: (xml: string) => { // Note: This would need the invoice object, not XML // In practice, we'd parse the XML to EInvoice first return []; } }); } /** * Load Schematron for a specific format and standard */ public async loadSchematron( standard: 'EN16931' | 'PEPPOL' | 'XRECHNUNG', format: 'UBL' | 'CII' ): Promise { const schematronPath = await this.getSchematronPath(standard, format); if (!schematronPath) { throw new Error(`No Schematron available for ${standard} ${format}`); } // Check if file exists try { await fs.access(schematronPath); } catch { throw new Error(`Schematron file not found: ${schematronPath}. Run 'npm run download-schematron' first.`); } await this.schematronValidator.loadSchematron(schematronPath, true); this.schematronLoaded = true; } /** * Get the path to the appropriate Schematron file */ private async getSchematronPath( standard: 'EN16931' | 'PEPPOL' | 'XRECHNUNG', format: 'UBL' | 'CII' ): Promise { const basePath = 'assets_downloaded/schematron'; // Map standard and format to file pattern const patterns: Record> = { EN16931: { UBL: 'EN16931-UBL-*.sch', CII: 'EN16931-CII-*.sch' }, PEPPOL: { UBL: 'PEPPOL-EN16931-UBL-*.sch', CII: 'PEPPOL-EN16931-CII-*.sch' }, XRECHNUNG: { UBL: 'XRechnung-UBL-*.sch', CII: 'XRechnung-CII-*.sch' } }; const pattern = patterns[standard]?.[format]; if (!pattern) return null; // Find matching files try { const files = await fs.readdir(basePath); const regex = new RegExp(pattern.replace('*', '.*')); const matches = files.filter(f => regex.test(f)); if (matches.length > 0) { // Return the most recent version (lexicographically last) matches.sort(); return path.join(basePath, matches[matches.length - 1]); } } catch { // Directory doesn't exist } return null; } /** * Validate an invoice using all available validators */ public async validate( invoice: EInvoice, xmlContent?: string, options: ValidationOptions = {} ): Promise { const startTime = Date.now(); const results: ValidationResult[] = []; // Determine format hint const formatHint = options.formatHint || this.detectFormat(xmlContent); // Run TypeScript validators if (options.checkCodeLists !== false) { results.push(...this.codeListValidator.validate(invoice)); } results.push(...this.businessRulesValidator.validate(invoice, options)); // Run Schematron validation if XML is provided and Schematron is loaded if (xmlContent && this.schematronLoaded) { try { const schematronResults = await this.schematronValidator.validate(xmlContent, { includeWarnings: !options.strictMode, parameters: { profile: options.profile } }); results.push(...schematronResults); } catch (error) { console.warn(`Schematron validation failed: ${error.message}`); } } // Calculate statistics const errorCount = results.filter(r => r.severity === 'error').length; const warningCount = results.filter(r => r.severity === 'warning').length; const infoCount = results.filter(r => r.severity === 'info').length; // Estimate rule coverage const totalRules = this.estimateTotalRules(options.profile); const rulesChecked = new Set(results.map(r => r.ruleId)).size; return { valid: errorCount === 0, profile: options.profile || 'EN16931', timestamp: new Date().toISOString(), validatorVersion: '1.0.0', rulesetVersion: '1.3.14', results, errorCount, warningCount, infoCount, rulesChecked, rulesTotal: totalRules, coverage: (rulesChecked / totalRules) * 100, validationTime: Date.now() - startTime, documentId: invoice.accountingDocId, documentType: invoice.accountingDocType, format: formatHint }; } /** * 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; } /** * Estimate total number of rules for a profile */ 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 detection */ public async validateAuto( invoice: EInvoice, xmlContent?: string ): Promise { // Auto-detect format const format = this.detectFormat(xmlContent); // Try to load appropriate Schematron if (format && !this.schematronLoaded) { try { await this.loadSchematron('EN16931', format); } catch (error) { console.warn(`Could not load Schematron: ${error.message}`); } } return this.validate(invoice, xmlContent, { formatHint: format, checkCalculations: true, checkVAT: true, checkCodeLists: true }); } /** * Check if Schematron validation is available */ public hasSchematron(): boolean { return this.schematronLoaded; } /** * Get available Schematron files */ public async getAvailableSchematron(): Promise> { const available: Array<{ standard: string; format: string; path: string }> = []; for (const standard of ['EN16931', 'PEPPOL', 'XRECHNUNG'] as const) { for (const format of ['UBL', 'CII'] as const) { const schematronPath = await this.getSchematronPath(standard, format); if (schematronPath) { available.push({ standard, format, path: schematronPath }); } } } return available; } } /** * Create a pre-configured validator for a specific standard */ export async function createStandardValidator( standard: 'EN16931' | 'PEPPOL' | 'XRECHNUNG', format: 'UBL' | 'CII' ): Promise { const validator = new IntegratedValidator(); try { await validator.loadSchematron(standard, format); } catch (error) { console.warn(`Schematron not available for ${standard} ${format}: ${error.message}`); } return validator; }