- 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
285 lines
8.3 KiB
TypeScript
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;
|
|
} |