BREAKING CHANGE(core): Rebrand XInvoice to EInvoice: update package name, class names, imports, and documentation

This commit is contained in:
2025-05-24 16:33:58 +00:00
parent 805adc6d5c
commit a93ea090ce
27 changed files with 3172 additions and 295 deletions

View File

@ -2,7 +2,7 @@
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@fin.cx/xinvoice',
version: '4.3.0',
description: 'A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for xinvoice packages.'
name: '@fin.cx/einvoice',
version: '5.0.0',
description: 'A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for electronic invoice (einvoice) packages.'
}

View File

@ -3,8 +3,17 @@ import * as plugins from './plugins.js';
import { business, finance } from './plugins.js';
import type { TInvoice } from './interfaces/common.js';
import { InvoiceFormat, ValidationLevel } from './interfaces/common.js';
import type { ValidationResult, ValidationError, XInvoiceOptions, IPdf, ExportFormat } from './interfaces/common.js';
// PDF-related imports are handled by the PDF utilities
import type { ValidationResult, ValidationError, EInvoiceOptions, IPdf, ExportFormat } from './interfaces/common.js';
// Import error classes
import {
EInvoiceError,
EInvoiceParsingError,
EInvoiceValidationError,
EInvoicePDFError,
EInvoiceFormatError,
ErrorContext
} from './errors.js';
// Import factories
import { DecoderFactory } from './formats/factories/decoder.factory.js';
@ -23,7 +32,7 @@ import { FormatDetector } from './formats/utils/format.detector.js';
* Supports various invoice formats including Factur-X, ZUGFeRD, UBL, and XRechnung
* Implements TInvoice interface for seamless integration with existing systems
*/
export class XInvoice {
export class EInvoice {
// TInvoice interface properties
public id: string = '';
public invoiceId: string = '';
@ -59,11 +68,11 @@ export class XInvoice {
public electronicAddress?: { scheme: string; value: string };
public paymentOptions?: finance.IPaymentOptionInfo;
// XInvoice specific properties
// EInvoice specific properties
private xmlString: string = '';
private detectedFormat: InvoiceFormat = InvoiceFormat.UNKNOWN;
private validationErrors: ValidationError[] = [];
private options: XInvoiceOptions = {
private options: EInvoiceOptions = {
validateOnLoad: false,
validationLevel: ValidationLevel.SYNTAX
};
@ -73,10 +82,10 @@ export class XInvoice {
private pdfExtractor = new PDFExtractor();
/**
* Creates a new XInvoice instance
* Creates a new EInvoice instance
* @param options Configuration options
*/
constructor(options?: XInvoiceOptions) {
constructor(options?: EInvoiceOptions) {
// Initialize empty contact objects
this.from = this.createEmptyContact();
this.to = this.createEmptyContact();
@ -117,42 +126,42 @@ export class XInvoice {
}
/**
* Creates a new XInvoice instance from XML
* Creates a new EInvoice instance from XML
* @param xmlString XML content
* @param options Configuration options
* @returns XInvoice instance
* @returns EInvoice instance
*/
public static async fromXml(xmlString: string, options?: XInvoiceOptions): Promise<XInvoice> {
const xinvoice = new XInvoice(options);
public static async fromXml(xmlString: string, options?: EInvoiceOptions): Promise<EInvoice> {
const einvoice = new EInvoice(options);
// Load XML data
await xinvoice.loadXml(xmlString);
await einvoice.loadXml(xmlString);
return xinvoice;
return einvoice;
}
/**
* Creates a new XInvoice instance from PDF
* Creates a new EInvoice instance from PDF
* @param pdfBuffer PDF buffer
* @param options Configuration options
* @returns XInvoice instance
* @returns EInvoice instance
*/
public static async fromPdf(pdfBuffer: Uint8Array | Buffer, options?: XInvoiceOptions): Promise<XInvoice> {
const xinvoice = new XInvoice(options);
public static async fromPdf(pdfBuffer: Uint8Array | Buffer, options?: EInvoiceOptions): Promise<EInvoice> {
const einvoice = new EInvoice(options);
// Load PDF data
await xinvoice.loadPdf(pdfBuffer);
await einvoice.loadPdf(pdfBuffer);
return xinvoice;
return einvoice;
}
/**
* Loads XML data into the XInvoice instance
* Loads XML data into the EInvoice instance
* @param xmlString XML content
* @param validate Whether to validate the XML
* @returns This instance for chaining
*/
public async loadXml(xmlString: string, validate: boolean = false): Promise<XInvoice> {
public async loadXml(xmlString: string, validate: boolean = false): Promise<EInvoice> {
this.xmlString = xmlString;
// Detect format
@ -173,20 +182,32 @@ export class XInvoice {
await this.validate(this.options.validationLevel);
}
} catch (error) {
console.error('Error loading XML:', error);
throw error;
const context = new ErrorContext()
.add('format', this.detectedFormat)
.add('xmlLength', xmlString.length)
.addTimestamp()
.build();
if (error instanceof Error) {
throw new EInvoiceParsingError(
`Failed to load XML: ${error.message}`,
{ format: this.detectedFormat.toString(), ...context },
error
);
}
throw new EInvoiceParsingError('Failed to load XML: Unknown error', context);
}
return this;
}
/**
* Loads PDF data into the XInvoice instance
* Loads PDF data into the EInvoice instance
* @param pdfBuffer PDF buffer
* @param validate Whether to validate the extracted XML
* @returns This instance for chaining
*/
public async loadPdf(pdfBuffer: Uint8Array | Buffer, validate: boolean = false): Promise<XInvoice> {
public async loadPdf(pdfBuffer: Uint8Array | Buffer, validate: boolean = false): Promise<EInvoice> {
try {
// Extract XML from PDF using the consolidated extractor
const extractResult = await this.pdfExtractor.extractXml(pdfBuffer);
@ -205,8 +226,18 @@ export class XInvoice {
// Handle extraction result
if (!extractResult.success || !extractResult.xml) {
const errorMessage = extractResult.error ? extractResult.error.message : 'Unknown error extracting XML from PDF';
console.warn('XML extraction failed:', errorMessage);
throw new Error(`No XML found in PDF: ${errorMessage}`);
throw new EInvoicePDFError(
`Failed to extract XML from PDF: ${errorMessage}`,
'extract',
{
pdfInfo: {
size: pdfBuffer.length,
filename: 'invoice.pdf'
},
extractionMethod: 'standard'
},
extractResult.error?.originalError
);
}
// Load the extracted XML
@ -217,8 +248,15 @@ export class XInvoice {
return this;
} catch (error) {
console.error('Error loading PDF:', error);
throw error;
if (error instanceof EInvoiceError) {
throw error; // Re-throw our errors
}
throw new EInvoicePDFError(
`Failed to load PDF: ${error instanceof Error ? error.message : 'Unknown error'}`,
'extract',
{ pdfSize: pdfBuffer.length },
error instanceof Error ? error : undefined
);
}
}
@ -265,7 +303,14 @@ export class XInvoice {
*/
public async validate(level: ValidationLevel = ValidationLevel.SYNTAX): Promise<ValidationResult> {
if (!this.xmlString) {
throw new Error('No XML to validate');
throw new EInvoiceValidationError(
'No XML content available for validation',
[{
code: 'VAL-001',
message: 'XML content must be loaded before validation',
severity: 'error'
}]
);
}
try {
@ -280,17 +325,26 @@ export class XInvoice {
return result;
} catch (error) {
console.error('Error validating XML:', error);
const errorResult: ValidationResult = {
valid: false,
errors: [{
const validationError = new EInvoiceValidationError(
`Validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
[{
code: 'VAL-ERROR',
message: `Validation error: ${error instanceof Error ? error.message : String(error)}`
message: error instanceof Error ? error.message : String(error),
severity: 'error'
}],
{
format: this.detectedFormat,
level
}
);
this.validationErrors = validationError.validationErrors;
return {
valid: false,
errors: validationError.validationErrors,
level
};
this.validationErrors = errorResult.errors;
return errorResult;
}
}
@ -330,7 +384,13 @@ export class XInvoice {
*/
public async exportPdf(format: ExportFormat = 'facturx'): Promise<IPdf> {
if (!this.pdf) {
throw new Error('No PDF data available. Use loadPdf() first or set the pdf property.');
throw new EInvoicePDFError(
'No PDF data available for export',
'create',
{
suggestion: 'Use loadPdf() first or set the pdf property before exporting'
}
);
}
// Generate XML in the specified format
@ -372,8 +432,19 @@ export class XInvoice {
// Handle potential errors
if (!result.success || !result.pdf) {
const errorMessage = result.error ? result.error.message : 'Unknown error embedding XML into PDF';
console.error('Error exporting PDF:', errorMessage);
throw new Error(`Failed to export PDF: ${errorMessage}`);
throw new EInvoicePDFError(
`Failed to embed XML into PDF: ${errorMessage}`,
'embed',
{
format,
xmlLength: xmlContent.length,
pdfInfo: {
filename: this.pdf.name,
size: this.pdf.buffer.length
}
},
result.error?.originalError
);
}
return result.pdf;

341
ts/errors.ts Normal file
View File

@ -0,0 +1,341 @@
/**
* 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);
}
}

View File

@ -1,5 +1,5 @@
// Import main class
import { XInvoice } from './classes.xinvoice.js';
import { EInvoice } from './einvoice.js';
// Import interfaces
import * as common from './interfaces/common.js';
@ -37,6 +37,17 @@ import {
// Import format detector
import { FormatDetector } from './formats/utils/format.detector.js';
// Import error classes
import {
EInvoiceError,
EInvoiceParsingError,
EInvoiceValidationError,
EInvoicePDFError,
EInvoiceFormatError,
ErrorRecovery,
ErrorContext
} from './errors.js';
// Import Factur-X implementation
import { FacturXDecoder } from './formats/cii/facturx/facturx.decoder.js';
import { FacturXEncoder } from './formats/cii/facturx/facturx.encoder.js';
@ -66,7 +77,7 @@ export type {
// Format interfaces
ExportFormat,
XInvoiceOptions
EInvoiceOptions
} from './interfaces/common.js';
export { ValidationLevel, InvoiceFormat } from './interfaces/common.js';
@ -75,7 +86,7 @@ export { ValidationLevel, InvoiceFormat } from './interfaces/common.js';
export { common as interfaces };
// Export main class
export { XInvoice };
export { EInvoice };
// Export factories
export { DecoderFactory, EncoderFactory, ValidatorFactory };
@ -108,6 +119,17 @@ export {
// Export format detector
export { FormatDetector };
// Export error classes
export {
EInvoiceError,
EInvoiceParsingError,
EInvoiceValidationError,
EInvoicePDFError,
EInvoiceFormatError,
ErrorRecovery,
ErrorContext
};
/**
* Validates an XML string against the appropriate format rules
* @param xml XML content to validate
@ -134,9 +156,9 @@ export function validateXml(
}
/**
* Creates a new XInvoice instance
* @returns A new XInvoice instance
* Creates a new EInvoice instance
* @returns A new EInvoice instance
*/
export function createXInvoice(): XInvoice {
return new XInvoice();
export function createEInvoice(): EInvoice {
return new EInvoice();
}

View File

@ -1,4 +1,4 @@
export interface IXInvoice {
export interface IEInvoice {
InvoiceNumber: string;
DateIssued: string; // Date in ISO 8601 format
Seller: IParty;
@ -81,9 +81,9 @@ export interface ValidationResult {
}
/**
* Options for the XInvoice class
* Options for the EInvoice class
*/
export interface XInvoiceOptions {
export interface EInvoiceOptions {
validateOnLoad?: boolean; // Whether to validate when loading an invoice
validationLevel?: ValidationLevel; // Level of validation to perform
}

View File

@ -48,9 +48,9 @@ export interface ValidationResult {
}
/**
* Options for the XInvoice class
* Options for the EInvoice class
*/
export interface XInvoiceOptions {
export interface EInvoiceOptions {
validateOnLoad?: boolean; // Whether to validate when loading an invoice
validationLevel?: ValidationLevel; // Level of validation to perform
}