123 lines
3.5 KiB
TypeScript
123 lines
3.5 KiB
TypeScript
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 } from 'xmldom';
|
|
import * as xpath from 'xpath';
|
|
|
|
/**
|
|
* Base decoder for UBL-based invoice formats
|
|
*/
|
|
export abstract class UBLBaseDecoder extends BaseDecoder {
|
|
protected doc: Document;
|
|
protected namespaces: Record<string, string>;
|
|
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<TInvoice> {
|
|
// 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<TCreditNote>;
|
|
|
|
/**
|
|
* Decodes a UBL debit note (invoice)
|
|
* @returns Promise resolving to a TDebitNote object
|
|
*/
|
|
protected abstract decodeDebitNote(): Promise<TDebitNote>;
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|