einvoice/ts/errors.ts

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, '&amp;')
.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 '&lt;';
});
// 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);
}
}