Files
einvoice/ts/formats/validation/schematron.validator.ts
Juergen Kunz 10e14af85b feat(validation): Implement EN16931 compliance validation types and VAT categories
- Added validation types for EN16931 compliance in `validation.types.ts`, including interfaces for `ValidationResult`, `ValidationOptions`, and `ValidationReport`.
- Introduced `VATCategoriesValidator` in `vat-categories.validator.ts` to validate VAT categories according to EN16931 rules, including detailed checks for standard, zero-rated, exempt, reverse charge, intra-community, export, and out-of-scope services.
- Enhanced `IEInvoiceMetadata` interface in `en16931-metadata.ts` to include additional fields required for full standards compliance, such as delivery information, payment information, allowances, and charges.
- Implemented helper methods for VAT calculations and validation logic to ensure accurate compliance with EN16931 standards.
2025-08-11 12:25:32 +00:00

348 lines
11 KiB
TypeScript

import * as plugins from '../../plugins.js';
import * as SaxonJS from 'saxon-js';
import type { ValidationResult } from './validation.types.js';
/**
* Schematron validation options
*/
export interface SchematronOptions {
phase?: string; // Schematron phase to activate
parameters?: Record<string, any>; // Parameters to pass to Schematron
includeWarnings?: boolean; // Include warning-level messages
maxErrors?: number; // Maximum errors before stopping
}
/**
* Schematron validation engine using Saxon-JS
* Provides official standards validation through Schematron rules
*/
export class SchematronValidator {
private compiledStylesheet: any;
private schematronRules: string;
private isCompiled: boolean = false;
constructor(schematronRules?: string) {
this.schematronRules = schematronRules || '';
}
/**
* Load Schematron rules from file or string
*/
public async loadSchematron(source: string, isFilePath: boolean = true): Promise<void> {
if (isFilePath) {
// Load from file
const smartfile = await import('@push.rocks/smartfile');
this.schematronRules = await smartfile.SmartFile.fromFilePath(source).then(f => f.contentBuffer.toString());
} else {
// Use provided string
this.schematronRules = source;
}
// Reset compilation state
this.isCompiled = false;
}
/**
* Compile Schematron to XSLT using ISO Schematron skeleton
*/
private async compileSchematron(): Promise<void> {
if (this.isCompiled) return;
// The Schematron to XSLT transformation requires the ISO Schematron skeleton
// For now, we'll use a simplified approach with direct XSLT generation
// In production, we would use the official ISO Schematron skeleton XSLTs
try {
// Convert Schematron to XSLT
// This is a simplified version - in production we'd use the full ISO skeleton
const xslt = this.generateXSLTFromSchematron(this.schematronRules);
// Compile the XSLT with Saxon-JS
this.compiledStylesheet = await SaxonJS.compile({
stylesheetText: xslt,
warnings: 'silent'
});
this.isCompiled = true;
} catch (error) {
throw new Error(`Failed to compile Schematron: ${error.message}`);
}
}
/**
* Validate an XML document against loaded Schematron rules
*/
public async validate(
xmlContent: string,
options: SchematronOptions = {}
): Promise<ValidationResult[]> {
if (!this.schematronRules) {
throw new Error('No Schematron rules loaded');
}
// Ensure Schematron is compiled
await this.compileSchematron();
const results: ValidationResult[] = [];
try {
// Transform the XML with the compiled Schematron XSLT
const transformResult = await SaxonJS.transform({
stylesheetInternal: this.compiledStylesheet,
sourceText: xmlContent,
destination: 'serialized',
stylesheetParams: options.parameters || {}
});
// Parse the SVRL (Schematron Validation Report Language) output
results.push(...this.parseSVRL(transformResult.principalResult));
// Apply options filters
if (!options.includeWarnings) {
return results.filter(r => r.severity !== 'warning');
}
if (options.maxErrors && results.filter(r => r.severity === 'error').length > options.maxErrors) {
return results.slice(0, options.maxErrors);
}
return results;
} catch (error) {
results.push({
ruleId: 'SCHEMATRON-ERROR',
source: 'SCHEMATRON',
severity: 'error',
message: `Schematron validation failed: ${error.message}`,
btReference: undefined,
bgReference: undefined
});
return results;
}
}
/**
* Parse SVRL output to ValidationResult array
*/
private parseSVRL(svrlXml: string): ValidationResult[] {
const results: ValidationResult[] = [];
// Parse SVRL XML
const parser = new plugins.xmldom.DOMParser();
const doc = parser.parseFromString(svrlXml, 'text/xml');
// Get all failed assertions and successful reports
const failedAsserts = doc.getElementsByTagName('svrl:failed-assert');
const successfulReports = doc.getElementsByTagName('svrl:successful-report');
// Process failed assertions (these are errors)
for (let i = 0; i < failedAsserts.length; i++) {
const assert = failedAsserts[i];
const result = this.extractValidationResult(assert, 'error');
if (result) results.push(result);
}
// Process successful reports (these can be warnings or info)
for (let i = 0; i < successfulReports.length; i++) {
const report = successfulReports[i];
const result = this.extractValidationResult(report, 'warning');
if (result) results.push(result);
}
return results;
}
/**
* Extract ValidationResult from SVRL element
*/
private extractValidationResult(
element: Element,
defaultSeverity: 'error' | 'warning'
): ValidationResult | null {
const text = element.getElementsByTagName('svrl:text')[0]?.textContent || '';
const location = element.getAttribute('location') || undefined;
const test = element.getAttribute('test') || '';
const id = element.getAttribute('id') || element.getAttribute('role') || 'UNKNOWN';
const flag = element.getAttribute('flag') || defaultSeverity;
// Determine severity from flag attribute
let severity: 'error' | 'warning' | 'info' = defaultSeverity;
if (flag.toLowerCase().includes('fatal') || flag.toLowerCase().includes('error')) {
severity = 'error';
} else if (flag.toLowerCase().includes('warning')) {
severity = 'warning';
} else if (flag.toLowerCase().includes('info')) {
severity = 'info';
}
// Extract BT/BG references if present
const btMatch = text.match(/\[BT-(\d+)\]/);
const bgMatch = text.match(/\[BG-(\d+)\]/);
return {
ruleId: id,
source: 'EN16931',
severity,
message: text,
syntaxPath: location,
btReference: btMatch ? `BT-${btMatch[1]}` : undefined,
bgReference: bgMatch ? `BG-${bgMatch[1]}` : undefined,
profile: 'EN16931'
};
}
/**
* Generate simplified XSLT from Schematron
* This is a placeholder - in production, use ISO Schematron skeleton
*/
private generateXSLTFromSchematron(schematron: string): string {
// This is a simplified transformation
// In production, we would use the official ISO Schematron skeleton XSLTs
// (iso_schematron_skeleton.xsl, iso_svrl_for_xslt2.xsl, etc.)
// For now, return a basic XSLT that creates SVRL output
return `<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:svrl="http://purl.oclc.org/dsdl/svrl">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<svrl:schematron-output>
<!-- This is a placeholder transformation -->
<!-- Real implementation would process Schematron patterns and rules -->
<svrl:active-pattern>
<xsl:attribute name="document">
<xsl:value-of select="base-uri(/)"/>
</xsl:attribute>
</svrl:active-pattern>
</svrl:schematron-output>
</xsl:template>
</xsl:stylesheet>`;
}
/**
* Check if validator has rules loaded
*/
public hasRules(): boolean {
return !!this.schematronRules;
}
/**
* Get list of available phases from Schematron
*/
public async getPhases(): Promise<string[]> {
if (!this.schematronRules) return [];
const parser = new plugins.xmldom.DOMParser();
const doc = parser.parseFromString(this.schematronRules, 'text/xml');
const phases = doc.getElementsByTagName('sch:phase');
const phaseNames: string[] = [];
for (let i = 0; i < phases.length; i++) {
const id = phases[i].getAttribute('id');
if (id) phaseNames.push(id);
}
return phaseNames;
}
/**
* Validate with specific phase activated
*/
public async validateWithPhase(
xmlContent: string,
phase: string,
options: SchematronOptions = {}
): Promise<ValidationResult[]> {
return this.validate(xmlContent, { ...options, phase });
}
}
/**
* Factory function to create validator with standard Schematron packs
*/
export async function createStandardValidator(
standard: 'EN16931' | 'XRECHNUNG' | 'PEPPOL' | 'FACTURX'
): Promise<SchematronValidator> {
const validator = new SchematronValidator();
// Load appropriate Schematron based on standard
// These paths would point to actual Schematron files in production
switch (standard) {
case 'EN16931':
// Would load from ConnectingEurope/eInvoicing-EN16931
await validator.loadSchematron('assets/schematron/en16931/EN16931-UBL-validation.sch');
break;
case 'XRECHNUNG':
// Would load from itplr-kosit/xrechnung-schematron
await validator.loadSchematron('assets/schematron/xrechnung/XRechnung-UBL-validation.sch');
break;
case 'PEPPOL':
// Would load from OpenPEPPOL/peppol-bis-invoice-3
await validator.loadSchematron('assets/schematron/peppol/PEPPOL-EN16931-UBL.sch');
break;
case 'FACTURX':
// Would load from Factur-X specific Schematron
await validator.loadSchematron('assets/schematron/facturx/Factur-X-EN16931-validation.sch');
break;
}
return validator;
}
/**
* Hybrid validator that combines TypeScript and Schematron validation
*/
export class HybridValidator {
private schematronValidator: SchematronValidator;
private tsValidators: Array<{ validate: (xml: string) => ValidationResult[] }> = [];
constructor(schematronValidator?: SchematronValidator) {
this.schematronValidator = schematronValidator || new SchematronValidator();
}
/**
* Add a TypeScript validator to the pipeline
*/
public addTSValidator(validator: { validate: (xml: string) => ValidationResult[] }): void {
this.tsValidators.push(validator);
}
/**
* Run all validators and merge results
*/
public async validate(
xmlContent: string,
options: SchematronOptions = {}
): Promise<ValidationResult[]> {
const results: ValidationResult[] = [];
// Run TypeScript validators first (faster, better UX)
for (const validator of this.tsValidators) {
try {
results.push(...validator.validate(xmlContent));
} catch (error) {
console.warn(`TS validator failed: ${error.message}`);
}
}
// Run Schematron validation if available
if (this.schematronValidator.hasRules()) {
try {
const schematronResults = await this.schematronValidator.validate(xmlContent, options);
results.push(...schematronResults);
} catch (error) {
console.warn(`Schematron validation failed: ${error.message}`);
}
}
// Deduplicate results by ruleId
const seen = new Set<string>();
return results.filter(r => {
if (seen.has(r.ruleId)) return false;
seen.add(r.ruleId);
return true;
});
}
}