xinvoice/ts/formats/ubl/ubl.validator.ts
2025-04-03 15:53:08 +00:00

135 lines
3.9 KiB
TypeScript

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 } from 'xmldom';
import * as xpath from 'xpath';
/**
* Base validator for UBL-based invoice formats
*/
export abstract class UBLBaseValidator extends BaseValidator {
protected doc: Document;
protected namespaces: Record<string, string>;
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;
}
}