import { InvoiceFormat } from '../../interfaces/common.js'; import { DOMParser } from 'xmldom'; import * as xpath from 'xpath'; 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') { // 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') { // 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')) { 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; } } }