341 lines
9.0 KiB
TypeScript
341 lines
9.0 KiB
TypeScript
/**
|
|
* Base error class for all EInvoice-related errors
|
|
*/
|
|
export class EInvoiceError extends Error {
|
|
public code: string;
|
|
public details?: any;
|
|
public cause?: Error;
|
|
|
|
constructor(message: string, code: string, details?: any, cause?: Error) {
|
|
super(message);
|
|
this.name = 'EInvoiceError';
|
|
this.code = code;
|
|
this.details = details;
|
|
this.cause = cause;
|
|
|
|
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
if (Error.captureStackTrace) {
|
|
Error.captureStackTrace(this, this.constructor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a detailed error message including cause if available
|
|
*/
|
|
public getDetailedMessage(): string {
|
|
let message = `${this.name} [${this.code}]: ${this.message}`;
|
|
if (this.details) {
|
|
message += `\nDetails: ${JSON.stringify(this.details, null, 2)}`;
|
|
}
|
|
if (this.cause) {
|
|
message += `\nCaused by: ${this.cause.message}`;
|
|
}
|
|
return message;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error thrown when XML parsing fails
|
|
*/
|
|
export class EInvoiceParsingError extends EInvoiceError {
|
|
public line?: number;
|
|
public column?: number;
|
|
public xmlSnippet?: string;
|
|
|
|
constructor(
|
|
message: string,
|
|
details?: {
|
|
line?: number;
|
|
column?: number;
|
|
xmlSnippet?: string;
|
|
format?: string;
|
|
},
|
|
cause?: Error
|
|
) {
|
|
super(message, 'PARSE_ERROR', details, cause);
|
|
this.name = 'EInvoiceParsingError';
|
|
this.line = details?.line;
|
|
this.column = details?.column;
|
|
this.xmlSnippet = details?.xmlSnippet;
|
|
}
|
|
|
|
/**
|
|
* Returns a user-friendly error message with location information
|
|
*/
|
|
public getLocationMessage(): string {
|
|
let message = this.message;
|
|
if (this.line !== undefined && this.column !== undefined) {
|
|
message += ` at line ${this.line}, column ${this.column}`;
|
|
}
|
|
if (this.xmlSnippet) {
|
|
message += `\nNear: ${this.xmlSnippet}`;
|
|
}
|
|
return message;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error thrown when validation fails
|
|
*/
|
|
export class EInvoiceValidationError extends EInvoiceError {
|
|
public validationErrors: Array<{
|
|
code: string;
|
|
message: string;
|
|
location?: string;
|
|
severity?: 'error' | 'warning';
|
|
}>;
|
|
|
|
constructor(
|
|
message: string,
|
|
validationErrors: Array<{
|
|
code: string;
|
|
message: string;
|
|
location?: string;
|
|
severity?: 'error' | 'warning';
|
|
}>,
|
|
details?: any
|
|
) {
|
|
super(message, 'VALIDATION_ERROR', details);
|
|
this.name = 'EInvoiceValidationError';
|
|
this.validationErrors = validationErrors;
|
|
}
|
|
|
|
/**
|
|
* Returns a formatted validation report
|
|
*/
|
|
public getValidationReport(): string {
|
|
let report = `${this.message}\n\nValidation errors:\n`;
|
|
for (const error of this.validationErrors) {
|
|
report += `- [${error.code}] ${error.message}`;
|
|
if (error.location) {
|
|
report += ` (at ${error.location})`;
|
|
}
|
|
if (error.severity) {
|
|
report += ` [${error.severity.toUpperCase()}]`;
|
|
}
|
|
report += '\n';
|
|
}
|
|
return report;
|
|
}
|
|
|
|
/**
|
|
* Gets validation errors by severity
|
|
*/
|
|
public getErrorsBySeverity(severity: 'error' | 'warning'): typeof this.validationErrors {
|
|
return this.validationErrors.filter(e => (e.severity || 'error') === severity);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error thrown during PDF operations
|
|
*/
|
|
export class EInvoicePDFError extends EInvoiceError {
|
|
public operation: 'extract' | 'embed' | 'create' | 'validate';
|
|
public pdfInfo?: {
|
|
filename?: string;
|
|
size?: number;
|
|
pageCount?: number;
|
|
};
|
|
|
|
constructor(
|
|
message: string,
|
|
operation: 'extract' | 'embed' | 'create' | 'validate',
|
|
details?: any,
|
|
cause?: Error
|
|
) {
|
|
super(message, `PDF_${operation.toUpperCase()}_ERROR`, details, cause);
|
|
this.name = 'EInvoicePDFError';
|
|
this.operation = operation;
|
|
this.pdfInfo = details?.pdfInfo;
|
|
}
|
|
|
|
/**
|
|
* Returns recovery suggestions based on the operation
|
|
*/
|
|
public getRecoverySuggestions(): string[] {
|
|
const suggestions: string[] = [];
|
|
|
|
switch (this.operation) {
|
|
case 'extract':
|
|
suggestions.push(
|
|
'Ensure the PDF contains embedded XML data',
|
|
'Check if the PDF is a valid PDF/A-3 document',
|
|
'Try using a different extraction method',
|
|
'Verify the PDF is not corrupted'
|
|
);
|
|
break;
|
|
case 'embed':
|
|
suggestions.push(
|
|
'Ensure the source PDF is valid',
|
|
'Check that the XML data is well-formed',
|
|
'Verify sufficient memory is available',
|
|
'Try with a smaller XML payload'
|
|
);
|
|
break;
|
|
case 'create':
|
|
suggestions.push(
|
|
'Verify all required invoice data is provided',
|
|
'Check that the template PDF exists',
|
|
'Ensure write permissions for output directory'
|
|
);
|
|
break;
|
|
case 'validate':
|
|
suggestions.push(
|
|
'Check PDF/A-3 compliance',
|
|
'Verify XML attachment structure',
|
|
'Ensure proper PDF metadata'
|
|
);
|
|
break;
|
|
}
|
|
|
|
return suggestions;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error thrown for format-specific issues
|
|
*/
|
|
export class EInvoiceFormatError extends EInvoiceError {
|
|
public sourceFormat?: string;
|
|
public targetFormat?: string;
|
|
public unsupportedFeatures?: string[];
|
|
|
|
constructor(
|
|
message: string,
|
|
details: {
|
|
sourceFormat?: string;
|
|
targetFormat?: string;
|
|
unsupportedFeatures?: string[];
|
|
conversionPath?: string;
|
|
},
|
|
cause?: Error
|
|
) {
|
|
super(message, 'FORMAT_ERROR', details, cause);
|
|
this.name = 'EInvoiceFormatError';
|
|
this.sourceFormat = details.sourceFormat;
|
|
this.targetFormat = details.targetFormat;
|
|
this.unsupportedFeatures = details.unsupportedFeatures;
|
|
}
|
|
|
|
/**
|
|
* Returns a compatibility report
|
|
*/
|
|
public getCompatibilityReport(): string {
|
|
let report = this.message;
|
|
if (this.sourceFormat && this.targetFormat) {
|
|
report += `\n\nConversion: ${this.sourceFormat} → ${this.targetFormat}`;
|
|
}
|
|
if (this.unsupportedFeatures && this.unsupportedFeatures.length > 0) {
|
|
report += '\n\nUnsupported features:';
|
|
for (const feature of this.unsupportedFeatures) {
|
|
report += `\n- ${feature}`;
|
|
}
|
|
}
|
|
return report;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error recovery helper class
|
|
*/
|
|
export class ErrorRecovery {
|
|
/**
|
|
* Attempts to recover from a parsing error by cleaning the XML
|
|
*/
|
|
public static async attemptXMLRecovery(
|
|
xmlString: string,
|
|
error: EInvoiceParsingError
|
|
): Promise<{ success: boolean; cleanedXml?: string; message: string }> {
|
|
try {
|
|
let cleanedXml = xmlString;
|
|
|
|
// Remove BOM if present
|
|
if (cleanedXml.charCodeAt(0) === 0xFEFF) {
|
|
cleanedXml = cleanedXml.slice(1);
|
|
}
|
|
|
|
// Fix common encoding issues
|
|
cleanedXml = cleanedXml
|
|
.replace(/&(?!(amp|lt|gt|apos|quot);)/g, '&')
|
|
.replace(/</g, (match, offset) => {
|
|
// Don't replace if it's part of <![CDATA[
|
|
const before = cleanedXml.substring(Math.max(0, offset - 10), offset);
|
|
if (before.includes('CDATA[')) return match;
|
|
return '<';
|
|
});
|
|
|
|
// Try to fix unclosed tags if we have location info
|
|
if (error.line && error.xmlSnippet) {
|
|
// This is a simplified approach - real implementation would be more sophisticated
|
|
const lines = cleanedXml.split('\n');
|
|
if (lines[error.line - 1]) {
|
|
// Attempt basic fixes based on the error
|
|
// This is just a placeholder for more sophisticated recovery
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
cleanedXml,
|
|
message: 'Applied basic XML cleaning and encoding fixes'
|
|
};
|
|
} catch (recoveryError) {
|
|
return {
|
|
success: false,
|
|
message: `Recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides partial data extraction on validation failure
|
|
*/
|
|
public static extractPartialData(
|
|
xmlString: string,
|
|
format: string
|
|
): { success: boolean; partialData?: any; message: string } {
|
|
try {
|
|
// This would implement format-specific partial extraction
|
|
// For now, returning a placeholder
|
|
return {
|
|
success: false,
|
|
message: 'Partial data extraction not yet implemented'
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
message: `Partial extraction failed: ${error instanceof Error ? error.message : String(error)}`
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error context builder for detailed error information
|
|
*/
|
|
export class ErrorContext {
|
|
private context: Map<string, any> = new Map();
|
|
|
|
public add(key: string, value: any): this {
|
|
this.context.set(key, value);
|
|
return this;
|
|
}
|
|
|
|
public addTimestamp(): this {
|
|
this.context.set('timestamp', new Date().toISOString());
|
|
return this;
|
|
}
|
|
|
|
public addEnvironment(): this {
|
|
this.context.set('environment', {
|
|
nodeVersion: process.version,
|
|
platform: process.platform,
|
|
arch: process.arch
|
|
});
|
|
return this;
|
|
}
|
|
|
|
public build(): Record<string, any> {
|
|
return Object.fromEntries(this.context);
|
|
}
|
|
} |