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.
This commit is contained in:
285
ts/formats/validation/schematron.integration.ts
Normal file
285
ts/formats/validation/schematron.integration.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* 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/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;
|
||||
}
|
Reference in New Issue
Block a user