346 lines
9.2 KiB
TypeScript
346 lines
9.2 KiB
TypeScript
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<plugins.tsclass.business.ILetter> {
|
|
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,
|
|
};
|
|
}
|
|
} |