feat(core): improve in-memory validation, FatturaPA detection coverage, and published type compatibility
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { InvoiceFormat, ValidationLevel } from '../../../ts/interfaces/common.js';
|
||||
import { InvoiceFormat } from '../../../ts/interfaces/common.js';
|
||||
import { FormatDetector } from '../../../ts/formats/utils/format.detector.js';
|
||||
import { CorpusLoader } from '../../helpers/corpus.loader.js';
|
||||
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
@@ -15,264 +15,40 @@ import * as path from 'path';
|
||||
*/
|
||||
|
||||
tap.test('CORP-05: FatturaPA Corpus Processing - should process Italian FatturaPA files', async () => {
|
||||
// Load FatturaPA test files
|
||||
const fatturapaFiles = await CorpusLoader.loadCategory('FATTURAPA_OFFICIAL');
|
||||
|
||||
// Handle case where no files are found
|
||||
|
||||
if (fatturapaFiles.length === 0) {
|
||||
console.log('⚠ No FatturaPA files found in corpus - skipping test');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log(`Testing ${fatturapaFiles.length} FatturaPA files`);
|
||||
|
||||
const results = {
|
||||
total: fatturapaFiles.length,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
parseErrors: 0,
|
||||
validationErrors: 0,
|
||||
documentTypes: new Map<string, number>(),
|
||||
transmissionFormats: new Map<string, number>(),
|
||||
processingTimes: [] as number[]
|
||||
};
|
||||
|
||||
const failures: Array<{
|
||||
file: string;
|
||||
error: string;
|
||||
type: 'parse' | 'validation' | 'format';
|
||||
}> = [];
|
||||
|
||||
// Italian-specific validation patterns
|
||||
const italianValidations = {
|
||||
vatNumber: /^IT\d{11}$/,
|
||||
fiscalCode: /^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$/,
|
||||
invoiceNumber: /^\w+\/\d{4}$/, // Common format: PREFIX/YEAR
|
||||
codiceDestinatario: /^[A-Z0-9]{6,7}$/,
|
||||
pecEmail: /^[a-zA-Z0-9._%+-]+@pec\.[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
};
|
||||
|
||||
|
||||
let detectedCount = 0;
|
||||
let unsupportedDecodeCount = 0;
|
||||
|
||||
for (const file of fatturapaFiles) {
|
||||
try {
|
||||
const xmlBuffer = await CorpusLoader.loadFile(file.path);
|
||||
const xmlString = xmlBuffer.toString('utf-8');
|
||||
|
||||
// Track performance
|
||||
const { result: invoice, metric } = await PerformanceTracker.track(
|
||||
'fatturapa-processing',
|
||||
async () => {
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
// FatturaPA has specific XML structure
|
||||
if (xmlString.includes('FatturaElettronica')) {
|
||||
// Process as FatturaPA
|
||||
await einvoice.fromXmlString(xmlString);
|
||||
einvoice.metadata = {
|
||||
...einvoice.metadata,
|
||||
format: InvoiceFormat.FATTURAPA
|
||||
};
|
||||
} else {
|
||||
throw new Error('Not a valid FatturaPA file');
|
||||
}
|
||||
|
||||
return einvoice;
|
||||
},
|
||||
{ file: file.path, size: file.size }
|
||||
);
|
||||
|
||||
results.processingTimes.push(metric.duration);
|
||||
|
||||
// Extract FatturaPA specific information
|
||||
const formatMatch = xmlString.match(/<FormatoTrasmissione>([^<]+)<\/FormatoTrasmissione>/);
|
||||
const typeMatch = xmlString.match(/<TipoDocumento>([^<]+)<\/TipoDocumento>/);
|
||||
|
||||
if (formatMatch) {
|
||||
const format = formatMatch[1];
|
||||
results.transmissionFormats.set(format, (results.transmissionFormats.get(format) || 0) + 1);
|
||||
}
|
||||
|
||||
if (typeMatch) {
|
||||
const docType = typeMatch[1];
|
||||
results.documentTypes.set(docType, (results.documentTypes.get(docType) || 0) + 1);
|
||||
}
|
||||
|
||||
// Validate Italian-specific fields
|
||||
const vatMatch = xmlString.match(/<IdCodice>(\d{11})<\/IdCodice>/);
|
||||
const cfMatch = xmlString.match(/<CodiceFiscale>([A-Z0-9]{16})<\/CodiceFiscale>/);
|
||||
const destMatch = xmlString.match(/<CodiceDestinatario>([A-Z0-9]{6,7})<\/CodiceDestinatario>/);
|
||||
|
||||
let italianFieldsValid = true;
|
||||
|
||||
if (vatMatch && !italianValidations.vatNumber.test('IT' + vatMatch[1])) {
|
||||
italianFieldsValid = false;
|
||||
console.log(` - Invalid VAT number format: ${vatMatch[1]}`);
|
||||
}
|
||||
|
||||
if (cfMatch && !italianValidations.fiscalCode.test(cfMatch[1])) {
|
||||
italianFieldsValid = false;
|
||||
console.log(` - Invalid Codice Fiscale format: ${cfMatch[1]}`);
|
||||
}
|
||||
|
||||
if (destMatch && !italianValidations.codiceDestinatario.test(destMatch[1])) {
|
||||
italianFieldsValid = false;
|
||||
console.log(` - Invalid Codice Destinatario: ${destMatch[1]}`);
|
||||
}
|
||||
|
||||
// Validate the parsed invoice
|
||||
try {
|
||||
const validationResult = await invoice.validate(ValidationLevel.BUSINESS);
|
||||
|
||||
if (validationResult.valid && italianFieldsValid) {
|
||||
results.successful++;
|
||||
console.log(`✓ ${path.basename(file.path)}: Successfully processed`);
|
||||
|
||||
// Log key information
|
||||
if (formatMatch) {
|
||||
console.log(` - Transmission format: ${formatMatch[1]}`);
|
||||
}
|
||||
if (typeMatch) {
|
||||
const docTypeMap: Record<string, string> = {
|
||||
'TD01': 'Fattura',
|
||||
'TD02': 'Acconto/Anticipo',
|
||||
'TD03': 'Acconto/Anticipo su parcella',
|
||||
'TD04': 'Nota di Credito',
|
||||
'TD05': 'Nota di Debito',
|
||||
'TD06': 'Parcella'
|
||||
};
|
||||
console.log(` - Document type: ${docTypeMap[typeMatch[1]] || typeMatch[1]}`);
|
||||
}
|
||||
} else {
|
||||
results.validationErrors++;
|
||||
failures.push({
|
||||
file: path.basename(file.path),
|
||||
error: validationResult.errors?.[0]?.message || 'Validation failed',
|
||||
type: 'validation'
|
||||
});
|
||||
}
|
||||
} catch (validationError: any) {
|
||||
results.validationErrors++;
|
||||
failures.push({
|
||||
file: path.basename(file.path),
|
||||
error: validationError.message,
|
||||
type: 'validation'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
results.failed++;
|
||||
|
||||
if (error.message.includes('Not a valid FatturaPA')) {
|
||||
failures.push({
|
||||
file: path.basename(file.path),
|
||||
error: 'Invalid FatturaPA format',
|
||||
type: 'format'
|
||||
});
|
||||
} else {
|
||||
results.parseErrors++;
|
||||
failures.push({
|
||||
file: path.basename(file.path),
|
||||
error: error.message,
|
||||
type: 'parse'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✗ ${path.basename(file.path)}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary report
|
||||
console.log('\n=== FatturaPA Corpus Processing Summary ===');
|
||||
console.log(`Total files: ${results.total}`);
|
||||
console.log(`Successful: ${results.successful} (${(results.successful/results.total*100).toFixed(1)}%)`);
|
||||
console.log(`Failed: ${results.failed}`);
|
||||
console.log(` - Parse errors: ${results.parseErrors}`);
|
||||
console.log(` - Validation errors: ${results.validationErrors}`);
|
||||
|
||||
console.log('\nTransmission Formats:');
|
||||
results.transmissionFormats.forEach((count, format) => {
|
||||
const formatMap: Record<string, string> = {
|
||||
'FPA12': 'Pubblica Amministrazione',
|
||||
'FPR12': 'Privati',
|
||||
'SDI11': 'Sistema di Interscambio v1.1'
|
||||
};
|
||||
console.log(` - ${format}: ${formatMap[format] || format} (${count} files)`);
|
||||
});
|
||||
|
||||
console.log('\nDocument Types:');
|
||||
results.documentTypes.forEach((count, type) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'TD01': 'Fattura (Invoice)',
|
||||
'TD02': 'Acconto/Anticipo (Advance)',
|
||||
'TD03': 'Acconto/Anticipo su parcella',
|
||||
'TD04': 'Nota di Credito (Credit Note)',
|
||||
'TD05': 'Nota di Debito (Debit Note)',
|
||||
'TD06': 'Parcella (Fee Note)'
|
||||
};
|
||||
console.log(` - ${type}: ${typeMap[type] || type} (${count} files)`);
|
||||
});
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.log('\nFailure Details:');
|
||||
failures.forEach(f => {
|
||||
console.log(` ${f.file} [${f.type}]: ${f.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Performance metrics
|
||||
if (results.processingTimes.length > 0) {
|
||||
const avgTime = results.processingTimes.reduce((a, b) => a + b, 0) / results.processingTimes.length;
|
||||
const minTime = Math.min(...results.processingTimes);
|
||||
const maxTime = Math.max(...results.processingTimes);
|
||||
|
||||
console.log('\nPerformance Metrics:');
|
||||
console.log(` Average processing time: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(` Min time: ${minTime.toFixed(2)}ms`);
|
||||
console.log(` Max time: ${maxTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// FatturaPA specific features validation
|
||||
if (results.successful > 0 && fatturapaFiles.length > 0) {
|
||||
// Test a sample file for specific features
|
||||
const sampleFile = fatturapaFiles[0];
|
||||
const xmlBuffer = await CorpusLoader.loadFile(sampleFile.path);
|
||||
const xmlBuffer = await CorpusLoader.loadFile(file.path);
|
||||
const xmlString = xmlBuffer.toString('utf-8');
|
||||
|
||||
console.log('\nFatturaPA Structure Analysis:');
|
||||
|
||||
// Check for mandatory sections
|
||||
const mandatorySections = [
|
||||
'FatturaElettronicaHeader',
|
||||
'CedentePrestatore', // Seller
|
||||
'CessionarioCommittente', // Buyer
|
||||
'FatturaElettronicaBody',
|
||||
'DatiGenerali',
|
||||
'DatiBeniServizi'
|
||||
];
|
||||
|
||||
for (const section of mandatorySections) {
|
||||
if (xmlString.includes(section)) {
|
||||
console.log(`✓ Contains mandatory section: ${section}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for digital signature block
|
||||
if (xmlString.includes('<ds:Signature') || xmlString.includes('<Signature')) {
|
||||
console.log('✓ Contains digital signature block');
|
||||
const fileName = path.basename(file.path);
|
||||
|
||||
const format = FormatDetector.detectFormat(xmlString);
|
||||
expect(format).toEqual(InvoiceFormat.FATTURAPA);
|
||||
detectedCount++;
|
||||
|
||||
try {
|
||||
await EInvoice.fromXml(xmlString);
|
||||
expect(true).toBeFalse();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
expect(errorMessage.includes('FatturaPA decoder not yet implemented')).toBeTrue();
|
||||
unsupportedDecodeCount++;
|
||||
console.log(`✓ ${fileName}: Detection works and decode is explicitly unsupported`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all failures are due to unimplemented decoder
|
||||
const allNotImplemented = failures.every(f => f.error.includes('decoder not yet implemented'));
|
||||
|
||||
if (allNotImplemented && results.successful === 0) {
|
||||
console.log('\n⚠ FatturaPA decoder not yet implemented - test skipped');
|
||||
console.log(' This test will validate files once FatturaPA decoder is implemented');
|
||||
return; // Skip success criteria
|
||||
}
|
||||
|
||||
// Success criteria: at least 70% should pass (FatturaPA is complex)
|
||||
const successRate = results.successful / results.total;
|
||||
expect(successRate).toBeGreaterThan(0.7);
|
||||
|
||||
expect(detectedCount).toEqual(fatturapaFiles.length);
|
||||
expect(unsupportedDecodeCount).toEqual(fatturapaFiles.length);
|
||||
});
|
||||
|
||||
tap.start();
|
||||
tap.start();
|
||||
|
||||
Reference in New Issue
Block a user