import { BaseDecoder } from '../base/base.decoder.js'; import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js'; import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js'; import { DOMParser, xpath } from '../../plugins.js'; /** * Base decoder for UBL-based invoice formats */ export abstract class UBLBaseDecoder extends BaseDecoder { protected doc: Document; protected namespaces: Record; protected select: xpath.XPathSelect; constructor(xml: string) { super(xml); // Parse XML document this.doc = new DOMParser().parseFromString(xml, 'application/xml'); // Set up namespaces for XPath queries this.namespaces = { cbc: UBL_NAMESPACES.CBC, cac: UBL_NAMESPACES.CAC }; // Create XPath selector with namespaces this.select = xpath.useNamespaces(this.namespaces); } /** * Decodes UBL XML into a TInvoice object * @returns Promise resolving to a TInvoice object */ public async decode(): Promise { // Determine document type const documentType = this.getDocumentType(); if (documentType === UBLDocumentType.CREDIT_NOTE) { return this.decodeCreditNote(); } else { return this.decodeDebitNote(); } } /** * Gets the UBL document type * @returns UBL document type */ protected getDocumentType(): UBLDocumentType { const rootName = this.doc.documentElement.nodeName; if (rootName === UBLDocumentType.CREDIT_NOTE) { return UBLDocumentType.CREDIT_NOTE; } else { return UBLDocumentType.INVOICE; } } /** * Decodes a UBL credit note * @returns Promise resolving to a TCreditNote object */ protected abstract decodeCreditNote(): Promise; /** * Decodes a UBL 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; } }