Files
einvoice/ts/formats/validation/integrated.validator.ts
Juergen Kunz cbb297b0b1 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.
2025-08-11 18:07:01 +00:00

405 lines
13 KiB
TypeScript

/**
* 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';