import { BaseValidator } from '../base/base.validator.js'; import { ValidationLevel } from '../../interfaces/common.js'; import type { ValidationResult } from '../../interfaces/common.js'; import { UBLDocumentType } from './ubl.types.js'; import { DOMParser, xpath } from '../../plugins.js'; /** * Base validator for UBL-based invoice formats */ export abstract class UBLBaseValidator extends BaseValidator { protected doc: Document; protected namespaces: Record; protected select: xpath.XPathSelect; constructor(xml: string) { super(xml); try { // Parse XML document this.doc = new DOMParser().parseFromString(xml, 'application/xml'); // Set up namespaces for XPath queries this.namespaces = { cbc: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', cac: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2' }; // Create XPath selector with namespaces this.select = xpath.useNamespaces(this.namespaces); } catch (error) { this.addError('UBL-PARSE', `Failed to parse XML: ${error}`, '/'); } } /** * Validates UBL XML against the specified level of validation * @param level Validation level * @returns Result of validation */ public validate(level: ValidationLevel = ValidationLevel.SYNTAX): ValidationResult { // Reset errors this.errors = []; // Check if document was parsed successfully if (!this.doc) { return { valid: false, errors: this.errors, level: level }; } // Perform validation based on level let valid = true; if (level === ValidationLevel.SYNTAX) { valid = this.validateSchema(); } else if (level === ValidationLevel.SEMANTIC) { valid = this.validateSchema() && this.validateStructure(); } else if (level === ValidationLevel.BUSINESS) { valid = this.validateSchema() && this.validateStructure() && this.validateBusinessRules(); } return { valid, errors: this.errors, level }; } /** * Validates UBL XML against schema * @returns True if schema validation passed */ protected validateSchema(): boolean { // Basic schema validation (simplified for now) if (!this.doc) return false; // Check for root element const root = this.doc.documentElement; if (!root || (root.nodeName !== UBLDocumentType.INVOICE && root.nodeName !== UBLDocumentType.CREDIT_NOTE)) { this.addError('UBL-SCHEMA-1', `Root element must be ${UBLDocumentType.INVOICE} or ${UBLDocumentType.CREDIT_NOTE}`, '/'); return false; } return true; } /** * Validates structure of the UBL XML document * @returns True if structure validation passed */ protected abstract validateStructure(): boolean; /** * 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; } /** * 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; } }