Files
einvoice/ts/formats/validation/schematron.integration.ts
Juergen Kunz 58506e287d refactor: Move downloaded resources from assets/ to assets_downloaded/
- Changed default download location to assets_downloaded/schematron
- Updated all references in SchematronDownloader, integration, and validator
- Updated postinstall scripts to use new location
- assets_downloaded/ is already in .gitignore to exclude downloaded files from git
- Moved existing downloaded files to new location
- All functionality tested and working correctly
2025-08-12 05:25:50 +00:00

285 lines
8.3 KiB
TypeScript

/**
* 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<void> {
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<string | null> {
const basePath = 'assets_downloaded/schematron';
// Map standard and format to file pattern
const patterns: Record<string, Record<string, string>> = {
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<ValidationReport> {
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<string, number> = {
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<ValidationReport> {
// 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<Array<{
standard: string;
format: string;
path: string;
}>> {
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<IntegratedValidator> {
const validator = new IntegratedValidator();
try {
await validator.loadSchematron(standard, format);
} catch (error) {
console.warn(`Schematron not available for ${standard} ${format}: ${error.message}`);
}
return validator;
}