- 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.
348 lines
11 KiB
TypeScript
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;
|
|
});
|
|
}
|
|
} |