import { business, finance } from '@tsclass/tsclass'; 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 factories import { DecoderFactory } from './formats/factories/decoder.factory.js'; import { EncoderFactory } from './formats/factories/encoder.factory.js'; import { ValidatorFactory } from './formats/factories/validator.factory.js'; // Import PDF utilities import { PDFEmbedder } from './formats/pdf/pdf.embedder.js'; import { PDFExtractor } from './formats/pdf/pdf.extractor.js'; // Import format detector import { FormatDetector } from './formats/utils/format.detector.js'; /** * Main class for working with electronic invoices. * Supports various invoice formats including Factur-X, ZUGFeRD, UBL, and XRechnung * Implements TInvoice interface for seamless integration with existing systems */ export class XInvoice { // TInvoice interface properties public id: string = ''; public invoiceId: string = ''; public invoiceType: 'creditnote' | 'debitnote' = 'debitnote'; public versionInfo: business.TDocumentEnvelope['versionInfo'] = { type: 'draft', version: '1.0.0' }; public type: 'invoice' = 'invoice'; public date = Date.now(); public status: 'draft' | 'invoice' | 'paid' | 'refunded' = 'invoice'; public subject: string = ''; public from: business.TContact; public to: business.TContact; public incidenceId: string = ''; public language: string = 'en'; public legalContact?: business.TContact; public objectActions: any[] = []; public pdf: IPdf | null = null; public pdfAttachments: IPdf[] | null = null; public accentColor: string | null = null; public logoUrl: string | null = null; // Additional properties for invoice data public items: finance.TInvoiceItem[] = []; public dueInDays: number = 30; public reverseCharge: boolean = false; public currency: finance.TCurrency = 'EUR'; public notes: string[] = []; public periodOfPerformance?: { from: number; to: number }; public deliveryDate?: number; public buyerReference?: string; public electronicAddress?: { scheme: string; value: string }; public paymentOptions?: finance.IPaymentOptionInfo; // XInvoice specific properties private xmlString: string = ''; private detectedFormat: InvoiceFormat = InvoiceFormat.UNKNOWN; private validationErrors: ValidationError[] = []; private options: XInvoiceOptions = { validateOnLoad: false, validationLevel: ValidationLevel.SYNTAX }; // PDF utilities private pdfEmbedder = new PDFEmbedder(); private pdfExtractor = new PDFExtractor(); /** * Creates a new XInvoice instance * @param options Configuration options */ constructor(options?: XInvoiceOptions) { // Initialize empty contact objects this.from = this.createEmptyContact(); this.to = this.createEmptyContact(); // Apply options if provided if (options) { this.options = { ...this.options, ...options }; } } /** * Creates an empty TContact object */ private createEmptyContact(): business.TContact { return { name: '', type: 'company', description: '', address: { streetName: '', houseNumber: '0', city: '', country: '', postalCode: '' }, status: 'active', foundedDate: { year: 2000, month: 1, day: 1 }, registrationDetails: { vatId: '', registrationId: '', registrationName: '' } }; } /** * Creates a new XInvoice instance from XML * @param xmlString XML content * @param options Configuration options * @returns XInvoice instance */ public static async fromXml(xmlString: string, options?: XInvoiceOptions): Promise { const xinvoice = new XInvoice(options); // Load XML data await xinvoice.loadXml(xmlString); return xinvoice; } /** * Creates a new XInvoice instance from PDF * @param pdfBuffer PDF buffer * @param options Configuration options * @returns XInvoice instance */ public static async fromPdf(pdfBuffer: Uint8Array | Buffer, options?: XInvoiceOptions): Promise { const xinvoice = new XInvoice(options); // Load PDF data await xinvoice.loadPdf(pdfBuffer); return xinvoice; } /** * Loads XML data into the XInvoice 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 { this.xmlString = xmlString; // Detect format this.detectedFormat = FormatDetector.detectFormat(xmlString); try { // Initialize the decoder with the XML string using the factory const decoder = DecoderFactory.createDecoder(xmlString); // Decode the XML into a TInvoice object const invoice = await decoder.decode(); // Copy data from the decoded invoice this.copyInvoiceData(invoice); // Validate the XML if requested or if validateOnLoad is true if (validate || this.options.validateOnLoad) { await this.validate(this.options.validationLevel); } } catch (error) { console.error('Error loading XML:', error); throw error; } return this; } /** * Loads PDF data into the XInvoice 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 { try { // Extract XML from PDF using the consolidated extractor // which tries multiple extraction methods in sequence const xmlContent = await this.pdfExtractor.extractXml(pdfBuffer); // Store the PDF buffer this.pdf = { name: 'invoice.pdf', id: `invoice-${Date.now()}`, metadata: { textExtraction: '' }, buffer: pdfBuffer instanceof Buffer ? new Uint8Array(pdfBuffer) : pdfBuffer }; if (!xmlContent) { // No XML found in PDF console.warn('No XML found in PDF'); throw new Error('No XML found in PDF'); } // Load the extracted XML await this.loadXml(xmlContent, validate); return this; } catch (error) { console.error('Error loading PDF:', error); throw error; } } /** * Copies data from a TInvoice object * @param invoice Source invoice data */ private copyInvoiceData(invoice: TInvoice): void { // Copy basic properties this.id = invoice.id; this.invoiceId = invoice.invoiceId || invoice.id; this.invoiceType = invoice.invoiceType; this.versionInfo = { ...invoice.versionInfo }; this.type = invoice.type; this.date = invoice.date; this.status = invoice.status; this.subject = invoice.subject; this.from = { ...invoice.from }; this.to = { ...invoice.to }; this.incidenceId = invoice.incidenceId; this.language = invoice.language; this.legalContact = invoice.legalContact ? { ...invoice.legalContact } : undefined; this.objectActions = [...invoice.objectActions]; this.pdf = invoice.pdf; this.pdfAttachments = invoice.pdfAttachments; // Copy invoice-specific properties if (invoice.items) this.items = [...invoice.items]; if (invoice.dueInDays) this.dueInDays = invoice.dueInDays; if (invoice.reverseCharge !== undefined) this.reverseCharge = invoice.reverseCharge; if (invoice.currency) this.currency = invoice.currency; if (invoice.notes) this.notes = [...invoice.notes]; if (invoice.periodOfPerformance) this.periodOfPerformance = { ...invoice.periodOfPerformance }; if (invoice.deliveryDate) this.deliveryDate = invoice.deliveryDate; if (invoice.buyerReference) this.buyerReference = invoice.buyerReference; if (invoice.electronicAddress) this.electronicAddress = { ...invoice.electronicAddress }; if (invoice.paymentOptions) this.paymentOptions = { ...invoice.paymentOptions }; } /** * Validates the XML against the appropriate format rules * @param level Validation level (syntax, semantic, business) * @returns Validation result */ public async validate(level: ValidationLevel = ValidationLevel.SYNTAX): Promise { if (!this.xmlString) { throw new Error('No XML to validate'); } try { // Initialize the validator with the XML string const validator = ValidatorFactory.createValidator(this.xmlString); // Run validation const result = validator.validate(level); // Store validation errors this.validationErrors = result.errors; return result; } catch (error) { console.error('Error validating XML:', error); const errorResult: ValidationResult = { valid: false, errors: [{ code: 'VAL-ERROR', message: `Validation error: ${error.message}` }], level }; this.validationErrors = errorResult.errors; return errorResult; } } /** * Checks if the invoice is valid * @returns True if no validation errors were found */ public isValid(): boolean { return this.validationErrors.length === 0; } /** * Gets validation errors from the last validation * @returns Array of validation errors */ public getValidationErrors(): ValidationError[] { return this.validationErrors; } /** * Exports the invoice as XML in the specified format * @param format Target format (e.g., 'facturx', 'xrechnung') * @returns XML string in the specified format */ public async exportXml(format: ExportFormat = 'facturx'): Promise { // Create encoder for the specified format const encoder = EncoderFactory.createEncoder(format); // Generate XML return await encoder.encode(this as unknown as TInvoice); } /** * Exports the invoice as a PDF with embedded XML * @param format Target format (e.g., 'facturx', 'zugferd', 'xrechnung', 'ubl') * @returns PDF object with embedded XML */ public async exportPdf(format: ExportFormat = 'facturx'): Promise { if (!this.pdf) { throw new Error('No PDF data available. Use loadPdf() first or set the pdf property.'); } // Generate XML in the specified format const xmlContent = await this.exportXml(format); // Determine filename based on format let filename = 'invoice.xml'; let description = 'XML Invoice'; switch (format.toLowerCase()) { case 'facturx': filename = 'factur-x.xml'; description = 'Factur-X XML Invoice'; break; case 'zugferd': filename = 'zugferd-invoice.xml'; description = 'ZUGFeRD XML Invoice'; break; case 'xrechnung': filename = 'xrechnung.xml'; description = 'XRechnung XML Invoice'; break; case 'ubl': filename = 'ubl-invoice.xml'; description = 'UBL XML Invoice'; break; } // Embed XML into PDF const modifiedPdf = await this.pdfEmbedder.createPdfWithXml( this.pdf.buffer, xmlContent, filename, description, this.pdf.name, this.pdf.id ); return modifiedPdf; } /** * Gets the raw XML content * @returns XML string */ public getXml(): string { return this.xmlString; } /** * Gets the invoice format as an enum value * @returns InvoiceFormat enum value */ public getFormat(): InvoiceFormat { return this.detectedFormat; } /** * Checks if the invoice is in the specified format * @param format Format to check * @returns True if the invoice is in the specified format */ public isFormat(format: InvoiceFormat): boolean { return this.detectedFormat === format; } }