134 lines
5.1 KiB
TypeScript
134 lines
5.1 KiB
TypeScript
import { InvoiceFormat } from '../../interfaces/common.js';
|
|
import { DOMParser, xpath } from '../../plugins.js';
|
|
import { CII_PROFILE_IDS, ZUGFERD_V1_NAMESPACES } from '../cii/cii.types.js';
|
|
|
|
/**
|
|
* Utility class for detecting invoice formats
|
|
*/
|
|
export class FormatDetector {
|
|
/**
|
|
* Detects the format of an XML document
|
|
* @param xml XML content to analyze
|
|
* @returns Detected invoice format
|
|
*/
|
|
public static detectFormat(xml: string): InvoiceFormat {
|
|
try {
|
|
const doc = new DOMParser().parseFromString(xml, 'application/xml');
|
|
const root = doc.documentElement;
|
|
|
|
if (!root) {
|
|
return InvoiceFormat.UNKNOWN;
|
|
}
|
|
|
|
// UBL detection (Invoice or CreditNote root element)
|
|
if (root.nodeName === 'Invoice' || root.nodeName === 'CreditNote') {
|
|
// For simplicity, we'll treat all UBL documents as XRechnung for now
|
|
// In a real implementation, we would check for specific customization IDs
|
|
return InvoiceFormat.XRECHNUNG;
|
|
}
|
|
|
|
// Factur-X/ZUGFeRD detection (CrossIndustryInvoice or CrossIndustryDocument root element)
|
|
if (root.nodeName === 'rsm:CrossIndustryInvoice' || root.nodeName === 'CrossIndustryInvoice' ||
|
|
root.nodeName.endsWith(':CrossIndustryInvoice')) {
|
|
// Set up namespaces for XPath queries (ZUGFeRD v2/Factur-X)
|
|
const namespaces = {
|
|
rsm: 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
|
|
ram: 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'
|
|
};
|
|
|
|
// Create XPath selector with namespaces
|
|
const select = xpath.useNamespaces(namespaces);
|
|
|
|
// Look for profile identifier
|
|
const profileNode = select(
|
|
'string(//rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID)',
|
|
doc
|
|
);
|
|
|
|
if (profileNode) {
|
|
const profileText = profileNode.toString();
|
|
|
|
// Check for ZUGFeRD profiles
|
|
if (profileText.includes('zugferd') ||
|
|
profileText === CII_PROFILE_IDS.ZUGFERD_BASIC ||
|
|
profileText === CII_PROFILE_IDS.ZUGFERD_COMFORT ||
|
|
profileText === CII_PROFILE_IDS.ZUGFERD_EXTENDED) {
|
|
return InvoiceFormat.ZUGFERD;
|
|
}
|
|
|
|
// Check for Factur-X profiles
|
|
if (profileText.includes('factur-x') ||
|
|
profileText === CII_PROFILE_IDS.FACTURX_MINIMUM ||
|
|
profileText === CII_PROFILE_IDS.FACTURX_BASIC ||
|
|
profileText === CII_PROFILE_IDS.FACTURX_EN16931) {
|
|
return InvoiceFormat.FACTURX;
|
|
}
|
|
}
|
|
|
|
// If we can't determine the specific CII format, default to generic CII
|
|
return InvoiceFormat.CII;
|
|
}
|
|
|
|
// ZUGFeRD v1 detection (CrossIndustryDocument root element)
|
|
if (root.nodeName === 'rsm:CrossIndustryDocument' || root.nodeName === 'CrossIndustryDocument' ||
|
|
root.nodeName === 'ram:CrossIndustryDocument' || root.nodeName.endsWith(':CrossIndustryDocument')) {
|
|
|
|
// Check for ZUGFeRD v1 namespace in the document
|
|
const xmlString = xml.toString();
|
|
if (xmlString.includes('urn:ferd:CrossIndustryDocument:invoice:1p0') ||
|
|
xmlString.includes('urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12') ||
|
|
xmlString.includes('urn:ferd:CrossIndustryDocument') ||
|
|
xmlString.includes('zugferd') ||
|
|
xmlString.includes('ZUGFeRD')) {
|
|
return InvoiceFormat.ZUGFERD;
|
|
}
|
|
|
|
// Set up namespaces for XPath queries (ZUGFeRD v1)
|
|
try {
|
|
const namespaces = {
|
|
rsm: ZUGFERD_V1_NAMESPACES.RSM,
|
|
ram: ZUGFERD_V1_NAMESPACES.RAM
|
|
};
|
|
|
|
// Create XPath selector with namespaces
|
|
const select = xpath.useNamespaces(namespaces);
|
|
|
|
// Look for profile identifier
|
|
const profileNode = select(
|
|
'string(//rsm:SpecifiedExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID)',
|
|
doc
|
|
);
|
|
|
|
if (profileNode) {
|
|
const profileText = profileNode.toString();
|
|
|
|
// Check for ZUGFeRD v1 profiles
|
|
if (profileText.includes('ferd:CrossIndustryDocument:invoice:1p0') ||
|
|
profileText === CII_PROFILE_IDS.ZUGFERD_V1_BASIC ||
|
|
profileText === CII_PROFILE_IDS.ZUGFERD_V1_COMFORT ||
|
|
profileText === CII_PROFILE_IDS.ZUGFERD_V1_EXTENDED) {
|
|
return InvoiceFormat.ZUGFERD;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('Error in ZUGFeRD v1 XPath detection:', error);
|
|
}
|
|
|
|
// If we can't determine the specific profile but it's a CrossIndustryDocument, it's likely ZUGFeRD v1
|
|
return InvoiceFormat.ZUGFERD;
|
|
}
|
|
|
|
// FatturaPA detection would be implemented here
|
|
if (root.nodeName === 'FatturaElettronica' ||
|
|
(root.getAttribute('xmlns') && root.getAttribute('xmlns')!.includes('fatturapa.gov.it'))) {
|
|
return InvoiceFormat.FATTURAPA;
|
|
}
|
|
|
|
return InvoiceFormat.UNKNOWN;
|
|
} catch (error) {
|
|
console.error('Error detecting format:', error);
|
|
return InvoiceFormat.UNKNOWN;
|
|
}
|
|
}
|
|
}
|