391 lines
12 KiB
TypeScript
391 lines
12 KiB
TypeScript
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<string, any>['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<XInvoice> {
|
|
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<XInvoice> {
|
|
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<XInvoice> {
|
|
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<XInvoice> {
|
|
try {
|
|
// Extract XML from PDF
|
|
const xmlContent = await this.pdfExtractor.extractXml(pdfBuffer);
|
|
|
|
if (!xmlContent) {
|
|
throw new Error('No XML found in PDF');
|
|
}
|
|
|
|
// Store the PDF buffer
|
|
this.pdf = {
|
|
name: 'invoice.pdf',
|
|
id: `invoice-${Date.now()}`,
|
|
metadata: {
|
|
textExtraction: ''
|
|
},
|
|
buffer: pdfBuffer instanceof Buffer ? new Uint8Array(pdfBuffer) : pdfBuffer
|
|
};
|
|
|
|
// 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<ValidationResult> {
|
|
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<string> {
|
|
// 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<IPdf> {
|
|
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;
|
|
}
|
|
}
|