feat: Implement PEPPOL and XRechnung validators for compliance with e-invoice specifications
- Added PeppolValidator class to validate PEPPOL BIS 3.0 invoices, including checks for endpoint IDs, document type IDs, process IDs, party identification, and business rules. - Implemented validation for GLN check digits, document types, and transport protocols specific to PEPPOL. - Added XRechnungValidator class to validate XRechnung 3.0 invoices, focusing on German-specific requirements such as Leitweg-ID, payment details, seller contact, and tax registration. - Included validation for IBAN and BIC formats, ensuring compliance with SEPA regulations. - Established methods for checking B2G invoice indicators and validating mandatory fields for both validators.
This commit is contained in:
405
ts/formats/validation/integrated.validator.ts
Normal file
405
ts/formats/validation/integrated.validator.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* Main integrated validator combining all validation capabilities
|
||||
* Orchestrates TypeScript validators, Schematron, and profile-specific rules
|
||||
*/
|
||||
|
||||
import { IntegratedValidator } from './schematron.integration.js';
|
||||
import { XRechnungValidator } from './xrechnung.validator.js';
|
||||
import { PeppolValidator } from './peppol.validator.js';
|
||||
import { FacturXValidator } from './facturx.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';
|
||||
|
||||
/**
|
||||
* Main validator that combines all validation capabilities
|
||||
*/
|
||||
export class MainValidator {
|
||||
private integratedValidator: IntegratedValidator;
|
||||
private xrechnungValidator: XRechnungValidator;
|
||||
private peppolValidator: PeppolValidator;
|
||||
private facturxValidator: FacturXValidator;
|
||||
private businessRulesValidator: EN16931BusinessRulesValidator;
|
||||
private codeListValidator: CodeListValidator;
|
||||
private schematronEnabled: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.integratedValidator = new IntegratedValidator();
|
||||
this.xrechnungValidator = XRechnungValidator.create();
|
||||
this.peppolValidator = PeppolValidator.create();
|
||||
this.facturxValidator = FacturXValidator.create();
|
||||
this.businessRulesValidator = new EN16931BusinessRulesValidator();
|
||||
this.codeListValidator = new CodeListValidator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Schematron validation for better coverage
|
||||
*/
|
||||
public async initializeSchematron(
|
||||
profile?: 'EN16931' | 'PEPPOL' | 'XRECHNUNG'
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Check available Schematron files
|
||||
const available = await this.integratedValidator.getAvailableSchematron();
|
||||
|
||||
if (available.length === 0) {
|
||||
console.warn('No Schematron files available. Run: npm run download-schematron');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load appropriate Schematron based on profile
|
||||
const standard = profile || 'EN16931';
|
||||
const format = 'UBL'; // Default to UBL, can be made configurable
|
||||
|
||||
await this.integratedValidator.loadSchematron(
|
||||
standard === 'XRECHNUNG' ? 'EN16931' : standard, // XRechnung uses EN16931 as base
|
||||
format
|
||||
);
|
||||
|
||||
this.schematronEnabled = true;
|
||||
console.log(`Schematron validation enabled for ${standard} ${format}`);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to initialize Schematron: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an invoice with all available validators
|
||||
*/
|
||||
public async validate(
|
||||
invoice: EInvoice,
|
||||
xmlContent?: string,
|
||||
options: ValidationOptions = {}
|
||||
): Promise<ValidationReport> {
|
||||
const startTime = Date.now();
|
||||
const results: ValidationResult[] = [];
|
||||
|
||||
// Detect profile from invoice
|
||||
const profile = this.detectProfile(invoice);
|
||||
const mergedOptions: ValidationOptions = {
|
||||
...options,
|
||||
profile: profile as ValidationOptions['profile']
|
||||
};
|
||||
|
||||
// Run base validators
|
||||
if (options.checkCodeLists !== false) {
|
||||
results.push(...this.codeListValidator.validate(invoice));
|
||||
}
|
||||
|
||||
results.push(...this.businessRulesValidator.validate(invoice, mergedOptions));
|
||||
|
||||
// Run XRechnung-specific validation if applicable
|
||||
if (this.isXRechnungInvoice(invoice)) {
|
||||
const xrResults = this.xrechnungValidator.validateXRechnung(invoice);
|
||||
results.push(...xrResults);
|
||||
}
|
||||
|
||||
// Run PEPPOL-specific validation if applicable
|
||||
if (this.isPeppolInvoice(invoice)) {
|
||||
const peppolResults = this.peppolValidator.validatePeppol(invoice);
|
||||
results.push(...peppolResults);
|
||||
}
|
||||
|
||||
// Run Factur-X specific validation if applicable
|
||||
if (this.isFacturXInvoice(invoice)) {
|
||||
const facturxResults = this.facturxValidator.validateFacturX(invoice);
|
||||
results.push(...facturxResults);
|
||||
}
|
||||
|
||||
// Run Schematron validation if available and XML is provided
|
||||
if (this.schematronEnabled && xmlContent) {
|
||||
try {
|
||||
const schematronReport = await this.integratedValidator.validate(
|
||||
invoice,
|
||||
xmlContent,
|
||||
mergedOptions
|
||||
);
|
||||
// Extract only Schematron-specific results to avoid duplication
|
||||
const schematronResults = schematronReport.results.filter(
|
||||
r => r.source === 'SCHEMATRON'
|
||||
);
|
||||
results.push(...schematronResults);
|
||||
} catch (error) {
|
||||
console.warn(`Schematron validation error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates (same rule + same field)
|
||||
const uniqueResults = this.deduplicateResults(results);
|
||||
|
||||
// Calculate statistics
|
||||
const errorCount = uniqueResults.filter(r => r.severity === 'error').length;
|
||||
const warningCount = uniqueResults.filter(r => r.severity === 'warning').length;
|
||||
const infoCount = uniqueResults.filter(r => r.severity === 'info').length;
|
||||
|
||||
// Estimate coverage
|
||||
const totalRules = this.estimateTotalRules(profile);
|
||||
const rulesChecked = new Set(uniqueResults.map(r => r.ruleId)).size;
|
||||
const coverage = totalRules > 0 ? (rulesChecked / totalRules) * 100 : 0;
|
||||
|
||||
return {
|
||||
valid: errorCount === 0,
|
||||
profile: profile || 'EN16931',
|
||||
timestamp: new Date().toISOString(),
|
||||
validatorVersion: '2.0.0',
|
||||
rulesetVersion: '1.3.14',
|
||||
results: uniqueResults,
|
||||
errorCount,
|
||||
warningCount,
|
||||
infoCount,
|
||||
rulesChecked,
|
||||
rulesTotal: totalRules,
|
||||
coverage,
|
||||
validationTime: Date.now() - startTime,
|
||||
documentId: invoice.accountingDocId,
|
||||
documentType: invoice.accountingDocType,
|
||||
format: this.detectFormat(xmlContent)
|
||||
} as ValidationReport & { schematronEnabled: boolean };
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect profile from invoice metadata
|
||||
*/
|
||||
private detectProfile(invoice: EInvoice): string {
|
||||
const profileId = invoice.metadata?.profileId || '';
|
||||
const customizationId = invoice.metadata?.customizationId || '';
|
||||
|
||||
if (profileId.includes('xrechnung') || customizationId.includes('xrechnung')) {
|
||||
return 'XRECHNUNG_3.0';
|
||||
}
|
||||
|
||||
if (profileId.includes('peppol') || customizationId.includes('peppol') ||
|
||||
profileId.includes('urn:fdc:peppol.eu')) {
|
||||
return 'PEPPOL_BIS_3.0';
|
||||
}
|
||||
|
||||
if (profileId.includes('facturx') || customizationId.includes('facturx') ||
|
||||
profileId.includes('zugferd')) {
|
||||
// Try to detect specific Factur-X profile
|
||||
const facturxProfile = this.facturxValidator.detectProfile(invoice);
|
||||
if (facturxProfile) {
|
||||
return `FACTURX_${facturxProfile}`;
|
||||
}
|
||||
return 'FACTURX_EN16931';
|
||||
}
|
||||
|
||||
return 'EN16931';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if invoice is XRechnung
|
||||
*/
|
||||
private isXRechnungInvoice(invoice: EInvoice): boolean {
|
||||
const profileId = invoice.metadata?.profileId || '';
|
||||
const customizationId = invoice.metadata?.customizationId || '';
|
||||
|
||||
const xrechnungProfiles = [
|
||||
'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung',
|
||||
'urn:cen.eu:en16931:2017#conformant#urn:xeinkauf.de:kosit:xrechnung',
|
||||
'xrechnung'
|
||||
];
|
||||
|
||||
return xrechnungProfiles.some(profile =>
|
||||
profileId.toLowerCase().includes(profile.toLowerCase()) ||
|
||||
customizationId.toLowerCase().includes(profile.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if invoice is PEPPOL
|
||||
*/
|
||||
private isPeppolInvoice(invoice: EInvoice): boolean {
|
||||
const profileId = invoice.metadata?.profileId || '';
|
||||
const customizationId = invoice.metadata?.customizationId || '';
|
||||
|
||||
const peppolProfiles = [
|
||||
'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0',
|
||||
'peppol-bis-3',
|
||||
'peppol'
|
||||
];
|
||||
|
||||
return peppolProfiles.some(profile =>
|
||||
profileId.toLowerCase().includes(profile.toLowerCase()) ||
|
||||
customizationId.toLowerCase().includes(profile.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if invoice is Factur-X
|
||||
*/
|
||||
private isFacturXInvoice(invoice: EInvoice): boolean {
|
||||
const profileId = invoice.metadata?.profileId || '';
|
||||
const customizationId = invoice.metadata?.customizationId || '';
|
||||
const format = invoice.metadata?.format;
|
||||
|
||||
return format?.includes('facturx') ||
|
||||
profileId.toLowerCase().includes('facturx') ||
|
||||
customizationId.toLowerCase().includes('facturx') ||
|
||||
profileId.toLowerCase().includes('zugferd') ||
|
||||
customizationId.toLowerCase().includes('zugferd');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicate validation results
|
||||
*/
|
||||
private deduplicateResults(results: ValidationResult[]): ValidationResult[] {
|
||||
const seen = new Set<string>();
|
||||
const unique: ValidationResult[] = [];
|
||||
|
||||
for (const result of results) {
|
||||
const key = `${result.ruleId}|${result.field || ''}|${result.message}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
unique.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
return unique;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate total rules for coverage calculation
|
||||
*/
|
||||
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 and profile detection
|
||||
*/
|
||||
public async validateAuto(
|
||||
invoice: EInvoice,
|
||||
xmlContent?: string
|
||||
): Promise<ValidationReport> {
|
||||
// Auto-detect profile
|
||||
const profile = this.detectProfile(invoice);
|
||||
|
||||
// Initialize Schematron if not already done
|
||||
if (!this.schematronEnabled && xmlContent) {
|
||||
await this.initializeSchematron(
|
||||
profile.startsWith('XRECHNUNG') ? 'XRECHNUNG' :
|
||||
profile.startsWith('PEPPOL') ? 'PEPPOL' : 'EN16931'
|
||||
);
|
||||
}
|
||||
|
||||
return this.validate(invoice, xmlContent, {
|
||||
checkCalculations: true,
|
||||
checkVAT: true,
|
||||
checkCodeLists: true,
|
||||
strictMode: profile.includes('XRECHNUNG') // Strict for XRechnung
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation capabilities
|
||||
*/
|
||||
public getCapabilities(): {
|
||||
schematron: boolean;
|
||||
xrechnung: boolean;
|
||||
peppol: boolean;
|
||||
facturx: boolean;
|
||||
calculations: boolean;
|
||||
codeLists: boolean;
|
||||
} {
|
||||
return {
|
||||
schematron: this.schematronEnabled,
|
||||
xrechnung: true,
|
||||
peppol: true,
|
||||
facturx: true,
|
||||
calculations: true,
|
||||
codeLists: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format validation report as text
|
||||
*/
|
||||
public formatReport(report: ValidationReport): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push('=== Validation Report ===');
|
||||
lines.push(`Profile: ${report.profile}`);
|
||||
lines.push(`Valid: ${report.valid ? '✅' : '❌'}`);
|
||||
lines.push(`Timestamp: ${report.timestamp}`);
|
||||
lines.push('');
|
||||
|
||||
if (report.errorCount > 0) {
|
||||
lines.push(`Errors: ${report.errorCount}`);
|
||||
report.results
|
||||
.filter(r => r.severity === 'error')
|
||||
.forEach(r => {
|
||||
lines.push(` ❌ [${r.ruleId}] ${r.message}`);
|
||||
if (r.field) lines.push(` Field: ${r.field}`);
|
||||
});
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (report.warningCount > 0) {
|
||||
lines.push(`Warnings: ${report.warningCount}`);
|
||||
report.results
|
||||
.filter(r => r.severity === 'warning')
|
||||
.forEach(r => {
|
||||
lines.push(` ⚠️ [${r.ruleId}] ${r.message}`);
|
||||
if (r.field) lines.push(` Field: ${r.field}`);
|
||||
});
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('Statistics:');
|
||||
lines.push(` Rules checked: ${report.rulesChecked}/${report.rulesTotal}`);
|
||||
lines.push(` Coverage: ${report.coverage.toFixed(1)}%`);
|
||||
lines.push(` Validation time: ${report.validationTime}ms`);
|
||||
|
||||
if ((report as any).schematronEnabled) {
|
||||
lines.push(' Schematron: ✅ Enabled');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pre-configured validator instance
|
||||
*/
|
||||
export async function createValidator(
|
||||
options: {
|
||||
profile?: 'EN16931' | 'PEPPOL' | 'XRECHNUNG';
|
||||
enableSchematron?: boolean;
|
||||
} = {}
|
||||
): Promise<MainValidator> {
|
||||
const validator = new MainValidator();
|
||||
|
||||
if (options.enableSchematron !== false) {
|
||||
await validator.initializeSchematron(options.profile);
|
||||
}
|
||||
|
||||
return validator;
|
||||
}
|
||||
|
||||
// Export for convenience
|
||||
export type { ValidationReport, ValidationResult, ValidationOptions } from './validation.types.js';
|
Reference in New Issue
Block a user