import { BaseDecoder } from '../base/base.decoder.js'; import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js'; import { CII_NAMESPACES, CIIProfile } from './cii.types.js'; import { DOMParser } from 'xmldom'; import * as xpath from 'xpath'; /** * Base decoder for CII-based invoice formats */ export abstract class CIIBaseDecoder extends BaseDecoder { protected doc: Document; protected namespaces: Record; protected select: xpath.XPathSelect; protected profile: CIIProfile = CIIProfile.EN16931; constructor(xml: string) { super(xml); // Parse XML document this.doc = new DOMParser().parseFromString(xml, 'application/xml'); // Set up namespaces for XPath queries this.namespaces = { rsm: CII_NAMESPACES.RSM, ram: CII_NAMESPACES.RAM, udt: CII_NAMESPACES.UDT }; // Create XPath selector with namespaces this.select = xpath.useNamespaces(this.namespaces); // Detect profile this.detectProfile(); } /** * Decodes CII XML into a TInvoice object * @returns Promise resolving to a TInvoice object */ public async decode(): Promise { // Determine if it's a credit note or debit note based on type code const typeCode = this.getText('//ram:TypeCode'); if (typeCode === '381') { // Credit note type code return this.decodeCreditNote(); } else { return this.decodeDebitNote(); } } /** * Detects the CII profile from the XML */ protected detectProfile(): void { // Look for profile identifier const profileNode = this.select( 'string(//rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID)', this.doc ); if (profileNode) { const profileText = profileNode.toString(); if (profileText.includes('BASIC')) { this.profile = CIIProfile.BASIC; } else if (profileText.includes('EN16931')) { this.profile = CIIProfile.EN16931; } else if (profileText.includes('EXTENDED')) { this.profile = CIIProfile.EXTENDED; } else if (profileText.includes('MINIMUM')) { this.profile = CIIProfile.MINIMUM; } else if (profileText.includes('COMFORT')) { this.profile = CIIProfile.COMFORT; } } } /** * Decodes a CII credit note * @returns Promise resolving to a TCreditNote object */ protected abstract decodeCreditNote(): Promise; /** * Decodes a CII debit note (invoice) * @returns Promise resolving to a TDebitNote object */ protected abstract decodeDebitNote(): Promise; /** * Gets a text value from an XPath expression * @param xpath XPath expression * @param context Optional context node * @returns Text value or empty string if not found */ protected getText(xpathExpr: string, context?: Node): string { const node = this.select(xpathExpr, context || this.doc)[0]; return node ? (node.textContent || '') : ''; } /** * Gets a number value from an XPath expression * @param xpath XPath expression * @param context Optional context node * @returns Number value or 0 if not found or not a number */ protected getNumber(xpathExpr: string, context?: Node): number { const text = this.getText(xpathExpr, context); const num = parseFloat(text); return isNaN(num) ? 0 : num; } /** * Gets a date value from an XPath expression * @param xpath XPath expression * @param context Optional context node * @returns Date timestamp or current time if not found or invalid */ protected getDate(xpathExpr: string, context?: Node): number { const text = this.getText(xpathExpr, context); if (!text) return Date.now(); const date = new Date(text); return isNaN(date.getTime()) ? Date.now() : date.getTime(); } /** * Checks if a node exists * @param xpath XPath expression * @param context Optional context node * @returns True if node exists */ protected exists(xpathExpr: string, context?: Node): boolean { const nodes = this.select(xpathExpr, context || this.doc); if (Array.isArray(nodes)) { return nodes.length > 0; } return false; } }