import * as plugins from './plugins.js'; import * as xmldom from 'xmldom'; /** * A class to convert a given XML string (ZUGFeRD/Factur-X, UBL or fatturaPA) * into a structured ILetter with invoice data. * * Handles different invoice XML formats: * - ZUGFeRD/Factur-X (CII) * - UBL * - FatturaPA */ export class ZUGFeRDXmlDecoder { private xmlString: string; private xmlFormat: string; private xmlDoc: Document | null = null; constructor(xmlString: string) { if (!xmlString) { throw new Error('No XML string provided to decoder'); } this.xmlString = xmlString; // Simple format detection based on string contents this.xmlFormat = this.detectFormat(); // Parse XML to DOM try { const parser = new xmldom.DOMParser(); this.xmlDoc = parser.parseFromString(this.xmlString, 'text/xml'); } catch (error) { console.error('Error parsing XML:', error); } } /** * Detects the XML invoice format using simple string checks */ private detectFormat(): string { // ZUGFeRD/Factur-X (CII format) if (this.xmlString.includes('CrossIndustryInvoice') || this.xmlString.includes('un/cefact') || this.xmlString.includes('rsm:')) { return 'CII'; } // UBL format if (this.xmlString.includes('Invoice') || this.xmlString.includes('oasis:names:specification:ubl')) { return 'UBL'; } // FatturaPA format if (this.xmlString.includes('FatturaElettronica') || this.xmlString.includes('fatturapa.gov.it')) { return 'FatturaPA'; } // Default to generic return 'unknown'; } /** * Extracts text from the first element matching the XPath-like selector */ private getElementText(tagName: string): string { if (!this.xmlDoc) { return ''; } try { // Basic handling for namespaced tags let namespace = ''; let localName = tagName; if (tagName.includes(':')) { const parts = tagName.split(':'); namespace = parts[0]; localName = parts[1]; } // Find all elements with this name const elements = this.xmlDoc.getElementsByTagName(tagName); if (elements.length > 0) { return elements[0].textContent || ''; } // Try with just the local name if we didn't find it with the namespace if (namespace) { const elements = this.xmlDoc.getElementsByTagName(localName); if (elements.length > 0) { return elements[0].textContent || ''; } } return ''; } catch (error) { console.error(`Error extracting element ${tagName}:`, error); return ''; } } /** * Converts XML to a structured letter object */ public async getLetterData(): Promise { try { if (this.xmlFormat === 'CII') { return this.parseCII(); } else if (this.xmlFormat === 'UBL') { // For now, use the default implementation return this.parseGeneric(); } else if (this.xmlFormat === 'FatturaPA') { // For now, use the default implementation return this.parseGeneric(); } else { return this.parseGeneric(); } } catch (error) { console.error('Error converting XML to letter data:', error); // If all else fails, return a minimal letter object return this.createDefaultLetter(); } } /** * Parse CII (ZUGFeRD/Factur-X) formatted XML */ private parseCII(): plugins.tsclass.business.ILetter { // Extract invoice ID let invoiceId = this.getElementText('ram:ID'); if (!invoiceId) { // Try alternative locations invoiceId = this.getElementText('rsm:ExchangedDocument ram:ID') || 'Unknown'; } // Extract seller name let sellerName = this.getElementText('ram:Name'); if (!sellerName) { sellerName = this.getElementText('ram:SellerTradeParty ram:Name') || 'Unknown Seller'; } // Extract buyer name let buyerName = ''; // Try to find BuyerTradeParty Name specifically if (this.xmlDoc) { const buyerParties = this.xmlDoc.getElementsByTagName('ram:BuyerTradeParty'); if (buyerParties.length > 0) { const nameElements = buyerParties[0].getElementsByTagName('ram:Name'); if (nameElements.length > 0) { buyerName = nameElements[0].textContent || ''; } } } if (!buyerName) { buyerName = 'Unknown Buyer'; } // Create seller const seller: plugins.tsclass.business.IContact = { name: sellerName, type: 'company', description: sellerName, address: { streetName: this.getElementText('ram:LineOne') || 'Unknown', houseNumber: '0', // Required by IAddress interface city: this.getElementText('ram:CityName') || 'Unknown', country: this.getElementText('ram:CountryID') || 'Unknown', postalCode: this.getElementText('ram:PostcodeCode') || 'Unknown', }, }; // Create buyer const buyer: plugins.tsclass.business.IContact = { name: buyerName, type: 'company', description: buyerName, address: { streetName: 'Unknown', houseNumber: '0', city: 'Unknown', country: 'Unknown', postalCode: 'Unknown', }, }; // Extract invoice type let invoiceType = 'debitnote'; const typeCode = this.getElementText('ram:TypeCode'); if (typeCode === '381') { invoiceType = 'creditnote'; } // Create invoice data const invoiceData: plugins.tsclass.finance.IInvoice = { id: invoiceId, status: null, type: invoiceType as 'debitnote' | 'creditnote', billedBy: seller, billedTo: buyer, deliveryDate: Date.now(), dueInDays: 30, periodOfPerformance: null, printResult: null, currency: (this.getElementText('ram:InvoiceCurrencyCode') || 'EUR') as plugins.tsclass.finance.TCurrency, notes: [], items: [ { name: 'Item from XML', unitQuantity: 1, unitNetPrice: 0, vatPercentage: 0, position: 0, unitType: 'units', } ], reverseCharge: false, }; // Return a letter return { versionInfo: { type: 'draft', version: '1.0.0', }, type: 'invoice', date: Date.now(), subject: `Invoice: ${invoiceId}`, from: seller, to: buyer, content: { invoiceData: invoiceData, textData: null, timesheetData: null, contractData: null, }, needsCoverSheet: false, objectActions: [], pdf: null, incidenceId: null, language: null, legalContact: null, logoUrl: null, pdfAttachments: null, accentColor: null, }; } /** * Parse generic XML using default approach */ private parseGeneric(): plugins.tsclass.business.ILetter { // Create a default letter with some extraction attempts return this.createDefaultLetter(); } /** * Creates a default letter object with minimal data */ private createDefaultLetter(): plugins.tsclass.business.ILetter { // Create a default seller const seller: plugins.tsclass.business.IContact = { name: 'Unknown Seller', type: 'company', description: 'Unknown Seller', // Required by IContact interface address: { streetName: 'Unknown', houseNumber: '0', // Required by IAddress interface city: 'Unknown', country: 'Unknown', postalCode: 'Unknown', }, }; // Create a default buyer const buyer: plugins.tsclass.business.IContact = { name: 'Unknown Buyer', type: 'company', description: 'Unknown Buyer', // Required by IContact interface address: { streetName: 'Unknown', houseNumber: '0', // Required by IAddress interface city: 'Unknown', country: 'Unknown', postalCode: 'Unknown', }, }; // Create default invoice data const invoiceData: plugins.tsclass.finance.IInvoice = { id: 'Unknown', status: null, type: 'debitnote', billedBy: seller, billedTo: buyer, deliveryDate: Date.now(), dueInDays: 30, periodOfPerformance: null, printResult: null, currency: 'EUR' as plugins.tsclass.finance.TCurrency, notes: [], items: [ { name: 'Unknown Item', unitQuantity: 1, unitNetPrice: 0, vatPercentage: 0, position: 0, unitType: 'units', } ], reverseCharge: false, }; // Return a default letter return { versionInfo: { type: 'draft', version: '1.0.0', }, type: 'invoice', date: Date.now(), subject: `Extracted Invoice (${this.xmlFormat} format)`, from: seller, to: buyer, content: { invoiceData: invoiceData, textData: null, timesheetData: null, contractData: null, }, needsCoverSheet: false, objectActions: [], pdf: null, incidenceId: null, language: null, legalContact: null, logoUrl: null, pdfAttachments: null, accentColor: null, }; } }