update
This commit is contained in:
@ -1,89 +1,85 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from './interfaces.js';
|
||||
import {
|
||||
PDFDocument,
|
||||
PDFDict,
|
||||
PDFName,
|
||||
PDFRawStream,
|
||||
PDFArray,
|
||||
PDFString,
|
||||
} from 'pdf-lib';
|
||||
import { FacturXEncoder } from './formats/facturx.encoder.js';
|
||||
import { XInvoiceEncoder } from './formats/xrechnung.encoder.js';
|
||||
import { DecoderFactory } from './formats/decoder.factory.js';
|
||||
import { BaseDecoder } from './formats/base.decoder.js';
|
||||
import { ValidatorFactory } from './formats/validator.factory.js';
|
||||
import { BaseValidator } from './formats/base.validator.js';
|
||||
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 ILetter interface for seamless integration with existing systems
|
||||
* Implements TInvoice interface for seamless integration with existing systems
|
||||
*/
|
||||
export class XInvoice implements plugins.tsclass.business.ILetter {
|
||||
// ILetter interface properties
|
||||
public versionInfo: plugins.tsclass.business.ILetter['versionInfo'] = {
|
||||
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: plugins.tsclass.business.ILetter['type'] = 'invoice';
|
||||
public type: 'invoice' = 'invoice';
|
||||
public date = Date.now();
|
||||
public subject: plugins.tsclass.business.ILetter['subject'] = '';
|
||||
public from: plugins.tsclass.business.TContact;
|
||||
public to: plugins.tsclass.business.TContact;
|
||||
public content: {
|
||||
invoiceData: plugins.tsclass.finance.IInvoice;
|
||||
textData: null;
|
||||
timesheetData: null;
|
||||
contractData: null;
|
||||
};
|
||||
public needsCoverSheet: plugins.tsclass.business.ILetter['needsCoverSheet'] = false;
|
||||
public objectActions: plugins.tsclass.business.ILetter['objectActions'] = [];
|
||||
public pdf: plugins.tsclass.business.ILetter['pdf'] = null;
|
||||
public incidenceId: plugins.tsclass.business.ILetter['incidenceId'] = null;
|
||||
public language: plugins.tsclass.business.ILetter['language'] = null;
|
||||
public legalContact: plugins.tsclass.business.ILetter['legalContact'] = null;
|
||||
public logoUrl: plugins.tsclass.business.ILetter['logoUrl'] = null;
|
||||
public pdfAttachments: plugins.tsclass.business.ILetter['pdfAttachments'] = null;
|
||||
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 encoderFacturX = new FacturXEncoder();
|
||||
private encoderXInvoice = new XInvoiceEncoder();
|
||||
private decoderInstance: BaseDecoder | null = null;
|
||||
private validatorInstance: BaseValidator | null = null;
|
||||
|
||||
// Format of the invoice, if detected
|
||||
private detectedFormat: interfaces.InvoiceFormat = interfaces.InvoiceFormat.UNKNOWN;
|
||||
|
||||
// Validation errors from last validation
|
||||
private validationErrors: interfaces.ValidationError[] = [];
|
||||
|
||||
// Options for this XInvoice instance
|
||||
private options: interfaces.XInvoiceOptions = {
|
||||
private detectedFormat: InvoiceFormat = InvoiceFormat.UNKNOWN;
|
||||
private validationErrors: ValidationError[] = [];
|
||||
private options: XInvoiceOptions = {
|
||||
validateOnLoad: false,
|
||||
validationLevel: interfaces.ValidationLevel.SYNTAX
|
||||
validationLevel: ValidationLevel.SYNTAX
|
||||
};
|
||||
|
||||
|
||||
// PDF utilities
|
||||
private pdfEmbedder = new PDFEmbedder();
|
||||
private pdfExtractor = new PDFExtractor();
|
||||
|
||||
/**
|
||||
* Creates a new XInvoice instance
|
||||
* @param options Configuration options
|
||||
*/
|
||||
constructor(options?: interfaces.XInvoiceOptions) {
|
||||
// Initialize empty IContact objects
|
||||
constructor(options?: XInvoiceOptions) {
|
||||
// Initialize empty contact objects
|
||||
this.from = this.createEmptyContact();
|
||||
this.to = this.createEmptyContact();
|
||||
|
||||
// Initialize empty IInvoice
|
||||
this.content = {
|
||||
invoiceData: this.createEmptyInvoice(),
|
||||
textData: null,
|
||||
timesheetData: null,
|
||||
contractData: null
|
||||
};
|
||||
|
||||
// Initialize with default options and override with provided options
|
||||
|
||||
// Apply options if provided
|
||||
if (options) {
|
||||
this.options = { ...this.options, ...options };
|
||||
}
|
||||
@ -92,7 +88,7 @@ export class XInvoice implements plugins.tsclass.business.ILetter {
|
||||
/**
|
||||
* Creates an empty TContact object
|
||||
*/
|
||||
private createEmptyContact(): plugins.tsclass.business.TContact {
|
||||
private createEmptyContact(): business.TContact {
|
||||
return {
|
||||
name: '',
|
||||
type: 'company',
|
||||
@ -104,448 +100,291 @@ export class XInvoice implements plugins.tsclass.business.ILetter {
|
||||
country: '',
|
||||
postalCode: ''
|
||||
},
|
||||
registrationDetails: {
|
||||
vatId: '',
|
||||
registrationId: '',
|
||||
registrationName: ''
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: {
|
||||
year: 2000,
|
||||
month: 1,
|
||||
day: 1
|
||||
},
|
||||
closedDate: {
|
||||
year: 9999,
|
||||
month: 12,
|
||||
day: 31
|
||||
},
|
||||
status: 'active'
|
||||
registrationDetails: {
|
||||
vatId: '',
|
||||
registrationId: '',
|
||||
registrationName: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty IInvoice object
|
||||
*/
|
||||
private createEmptyInvoice(): plugins.tsclass.finance.IInvoice {
|
||||
return {
|
||||
id: '',
|
||||
status: null,
|
||||
type: 'debitnote',
|
||||
billedBy: this.createEmptyContact(),
|
||||
billedTo: this.createEmptyContact(),
|
||||
deliveryDate: Date.now(),
|
||||
dueInDays: 30,
|
||||
periodOfPerformance: null,
|
||||
printResult: null,
|
||||
currency: 'EUR' as plugins.tsclass.finance.TCurrency,
|
||||
notes: [],
|
||||
items: [],
|
||||
reverseCharge: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create XInvoice from XML string
|
||||
* 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?: interfaces.XInvoiceOptions): Promise<XInvoice> {
|
||||
public static async fromXml(xmlString: string, options?: XInvoiceOptions): Promise<XInvoice> {
|
||||
const xinvoice = new XInvoice(options);
|
||||
|
||||
|
||||
// Load XML data
|
||||
await xinvoice.loadXml(xmlString);
|
||||
|
||||
|
||||
return xinvoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create XInvoice from PDF buffer
|
||||
* 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?: interfaces.XInvoiceOptions): Promise<XInvoice> {
|
||||
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 this XInvoice instance
|
||||
* Loads XML data into the XInvoice instance
|
||||
* @param xmlString XML content
|
||||
* @param validate Whether to validate
|
||||
* @param validate Whether to validate the XML
|
||||
* @returns This instance for chaining
|
||||
*/
|
||||
public async loadXml(xmlString: string, validate: boolean = false): Promise<void> {
|
||||
// Basic XML validation - just check if it starts with <?xml
|
||||
if (!xmlString || !xmlString.trim().startsWith('<?xml')) {
|
||||
throw new Error('Invalid XML: Missing XML declaration');
|
||||
}
|
||||
|
||||
// Store the XML string
|
||||
public async loadXml(xmlString: string, validate: boolean = false): Promise<XInvoice> {
|
||||
this.xmlString = xmlString;
|
||||
|
||||
// Detect the format
|
||||
this.detectedFormat = this.determineFormat(xmlString);
|
||||
|
||||
// Initialize the decoder with the XML string using the factory
|
||||
this.decoderInstance = DecoderFactory.createDecoder(xmlString);
|
||||
|
||||
// Initialize the validator with the XML string using the factory
|
||||
this.validatorInstance = ValidatorFactory.createValidator(xmlString);
|
||||
|
||||
// Validate the XML if requested or if validateOnLoad is true
|
||||
if (validate || this.options.validateOnLoad) {
|
||||
await this.validate(this.options.validationLevel);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Parse XML to ILetter
|
||||
const letterData = await this.decoderInstance.getLetterData();
|
||||
|
||||
// Copy letter data to this object
|
||||
this.copyLetterData(letterData);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads PDF data into this XInvoice instance and extracts embedded XML if present
|
||||
* 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): Promise<void> {
|
||||
// Create a valid IPdf object
|
||||
this.pdf = {
|
||||
name: 'invoice.pdf',
|
||||
id: `invoice-${Date.now()}`,
|
||||
metadata: {
|
||||
textExtraction: ''
|
||||
},
|
||||
buffer: Uint8Array.from(pdfBuffer)
|
||||
};
|
||||
|
||||
public async loadPdf(pdfBuffer: Uint8Array | Buffer, validate: boolean = false): Promise<XInvoice> {
|
||||
try {
|
||||
// Try to extract embedded XML
|
||||
const xmlContent = await this.extractXmlFromPdf();
|
||||
|
||||
// If XML was found, load it
|
||||
if (xmlContent) {
|
||||
await this.loadXml(xmlContent);
|
||||
// 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 extracting or parsing embedded XML from PDF:', error);
|
||||
console.error('Error loading PDF:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts XML from PDF
|
||||
* @returns XML content or null if not found
|
||||
* Copies data from a TInvoice object
|
||||
* @param invoice Source invoice data
|
||||
*/
|
||||
private async extractXmlFromPdf(): Promise<string> {
|
||||
if (!this.pdf) {
|
||||
throw new Error('No PDF data available');
|
||||
}
|
||||
|
||||
try {
|
||||
const pdfDoc = await PDFDocument.load(this.pdf.buffer);
|
||||
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;
|
||||
|
||||
// Get the document's metadata dictionary
|
||||
const namesDictObj = pdfDoc.catalog.lookup(PDFName.of('Names'));
|
||||
if (!(namesDictObj instanceof PDFDict)) {
|
||||
throw new Error('No Names dictionary found in PDF! This PDF does not contain embedded files.');
|
||||
}
|
||||
|
||||
const embeddedFilesDictObj = namesDictObj.lookup(PDFName.of('EmbeddedFiles'));
|
||||
if (!(embeddedFilesDictObj instanceof PDFDict)) {
|
||||
throw new Error('No EmbeddedFiles dictionary found! This PDF does not contain embedded files.');
|
||||
}
|
||||
|
||||
const filesSpecObj = embeddedFilesDictObj.lookup(PDFName.of('Names'));
|
||||
if (!(filesSpecObj instanceof PDFArray)) {
|
||||
throw new Error('No files specified in EmbeddedFiles dictionary!');
|
||||
}
|
||||
|
||||
// Try to find an XML file in the embedded files
|
||||
let xmlFile: PDFRawStream | undefined;
|
||||
let xmlFileName: string | undefined;
|
||||
|
||||
for (let i = 0; i < filesSpecObj.size(); i += 2) {
|
||||
const fileNameObj = filesSpecObj.lookup(i);
|
||||
const fileSpecObj = filesSpecObj.lookup(i + 1);
|
||||
|
||||
if (!(fileNameObj instanceof PDFString)) {
|
||||
continue;
|
||||
}
|
||||
if (!(fileSpecObj instanceof PDFDict)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the filename as string
|
||||
const fileName = fileNameObj.toString();
|
||||
|
||||
// Check if it's an XML file (checking both extension and known standard filenames)
|
||||
if (fileName.toLowerCase().includes('.xml') ||
|
||||
fileName.toLowerCase().includes('factur-x') ||
|
||||
fileName.toLowerCase().includes('zugferd') ||
|
||||
fileName.toLowerCase().includes('xrechnung')) {
|
||||
|
||||
const efDictObj = fileSpecObj.lookup(PDFName.of('EF'));
|
||||
if (!(efDictObj instanceof PDFDict)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const maybeStream = efDictObj.lookup(PDFName.of('F'));
|
||||
if (maybeStream instanceof PDFRawStream) {
|
||||
// Found an XML file - save it
|
||||
xmlFile = maybeStream;
|
||||
xmlFileName = fileName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no XML file was found, throw an error
|
||||
if (!xmlFile) {
|
||||
throw new Error('No embedded XML file found in the PDF!');
|
||||
}
|
||||
|
||||
// Decompress and decode the XML content
|
||||
const xmlCompressedBytes = xmlFile.getContents().buffer;
|
||||
const xmlBytes = plugins.pako.inflate(xmlCompressedBytes);
|
||||
const xmlContent = new TextDecoder('utf-8').decode(xmlBytes);
|
||||
|
||||
console.log(`Successfully extracted ${this.determineFormat(xmlContent)} XML from PDF file. File name: ${xmlFileName}`);
|
||||
|
||||
return xmlContent;
|
||||
} catch (error) {
|
||||
console.error('Error extracting or parsing embedded XML from PDF:', error);
|
||||
throw error;
|
||||
}
|
||||
// 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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies data from another ILetter object
|
||||
* @param letter Source letter data
|
||||
*/
|
||||
private copyLetterData(letter: plugins.tsclass.business.ILetter): void {
|
||||
this.versionInfo = { ...letter.versionInfo };
|
||||
this.type = letter.type;
|
||||
this.date = letter.date;
|
||||
this.subject = letter.subject;
|
||||
this.from = { ...letter.from };
|
||||
this.to = { ...letter.to };
|
||||
this.content = {
|
||||
invoiceData: letter.content.invoiceData ? { ...letter.content.invoiceData } : this.createEmptyInvoice(),
|
||||
textData: null,
|
||||
timesheetData: null,
|
||||
contractData: null
|
||||
};
|
||||
this.needsCoverSheet = letter.needsCoverSheet;
|
||||
this.objectActions = [...letter.objectActions];
|
||||
this.incidenceId = letter.incidenceId;
|
||||
this.language = letter.language;
|
||||
this.legalContact = letter.legalContact;
|
||||
this.logoUrl = letter.logoUrl;
|
||||
this.pdfAttachments = letter.pdfAttachments;
|
||||
this.accentColor = letter.accentColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the XML against the appropriate validation rules
|
||||
* Validates the XML against the appropriate format rules
|
||||
* @param level Validation level (syntax, semantic, business)
|
||||
* @returns Validation result
|
||||
*/
|
||||
public async validate(level: interfaces.ValidationLevel = interfaces.ValidationLevel.SYNTAX): Promise<interfaces.ValidationResult> {
|
||||
public async validate(level: ValidationLevel = ValidationLevel.SYNTAX): Promise<ValidationResult> {
|
||||
if (!this.xmlString) {
|
||||
throw new Error('No XML to validate');
|
||||
}
|
||||
|
||||
if (!this.validatorInstance) {
|
||||
// Initialize the validator with the XML string if not already done
|
||||
this.validatorInstance = ValidatorFactory.createValidator(this.xmlString);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Run validation
|
||||
const result = this.validatorInstance.validate(level);
|
||||
|
||||
// Store validation errors
|
||||
this.validationErrors = result.errors;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the document is valid based on the last validation
|
||||
* @returns True if the document is valid
|
||||
* Checks if the invoice is valid
|
||||
* @returns True if no validation errors were found
|
||||
*/
|
||||
public isValid(): boolean {
|
||||
if (!this.validatorInstance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.validatorInstance.isValid();
|
||||
return this.validationErrors.length === 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets validation errors from the last validation
|
||||
* @returns Array of validation errors
|
||||
*/
|
||||
public getValidationErrors(): interfaces.ValidationError[] {
|
||||
public getValidationErrors(): ValidationError[] {
|
||||
return this.validationErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the invoice to XML format
|
||||
* 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: interfaces.ExportFormat = 'facturx'): Promise<string> {
|
||||
format = format.toLowerCase() as interfaces.ExportFormat;
|
||||
|
||||
// Generate XML based on format
|
||||
switch (format) {
|
||||
case 'facturx':
|
||||
case 'zugferd':
|
||||
return this.encoderFacturX.createFacturXXml(this);
|
||||
|
||||
case 'xrechnung':
|
||||
case 'ubl':
|
||||
return this.encoderXInvoice.createXInvoiceXml(this);
|
||||
|
||||
default:
|
||||
// Default to Factur-X
|
||||
return this.encoderFacturX.createFacturXXml(this);
|
||||
}
|
||||
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 to PDF format with embedded XML
|
||||
* 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: interfaces.ExportFormat = 'facturx'): Promise<plugins.tsclass.business.IPdf> {
|
||||
format = format.toLowerCase() as interfaces.ExportFormat;
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate XML based on format
|
||||
const xmlContent = await this.exportXml(format);
|
||||
|
||||
// Load the PDF
|
||||
const pdfDoc = await PDFDocument.load(this.pdf.buffer);
|
||||
|
||||
// Convert the XML string to a Uint8Array
|
||||
const xmlBuffer = new TextEncoder().encode(xmlContent);
|
||||
|
||||
// Determine attachment filename based on format
|
||||
let filename = 'invoice.xml';
|
||||
let description = 'XML Invoice';
|
||||
|
||||
switch (format) {
|
||||
case 'facturx':
|
||||
filename = 'factur-x.xml';
|
||||
description = 'Factur-X XML Invoice';
|
||||
break;
|
||||
case 'xrechnung':
|
||||
filename = 'xrechnung.xml';
|
||||
description = 'XRechnung XML Invoice';
|
||||
break;
|
||||
}
|
||||
// Generate XML in the specified format
|
||||
const xmlContent = await this.exportXml(format);
|
||||
|
||||
// Make sure filename is lowercase (as required by documentation)
|
||||
filename = filename.toLowerCase();
|
||||
// Determine filename based on format
|
||||
let filename = 'invoice.xml';
|
||||
let description = 'XML Invoice';
|
||||
|
||||
// Use pdf-lib's .attach() to embed the XML
|
||||
pdfDoc.attach(xmlBuffer, filename, {
|
||||
mimeType: 'application/xml',
|
||||
description: description,
|
||||
});
|
||||
|
||||
// Save the modified PDF
|
||||
const modifiedPdfBytes = await pdfDoc.save();
|
||||
|
||||
// Update the pdf property with a proper IPdf object
|
||||
this.pdf = {
|
||||
name: this.pdf.name,
|
||||
id: this.pdf.id,
|
||||
metadata: this.pdf.metadata,
|
||||
buffer: modifiedPdfBytes
|
||||
};
|
||||
|
||||
return this.pdf;
|
||||
} catch (error) {
|
||||
console.error('Error embedding XML into PDF:', error);
|
||||
throw error;
|
||||
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(): interfaces.InvoiceFormat {
|
||||
public getFormat(): InvoiceFormat {
|
||||
return this.detectedFormat;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the invoice is in a specific format
|
||||
* 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: interfaces.InvoiceFormat): boolean {
|
||||
public isFormat(format: InvoiceFormat): boolean {
|
||||
return this.detectedFormat === format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the format of an XML document and returns the format enum
|
||||
* @param xmlContent XML content as string
|
||||
* @returns InvoiceFormat enum value
|
||||
*/
|
||||
private determineFormat(xmlContent: string): interfaces.InvoiceFormat {
|
||||
if (!xmlContent) {
|
||||
return interfaces.InvoiceFormat.UNKNOWN;
|
||||
}
|
||||
|
||||
// Check for ZUGFeRD/CII/Factur-X
|
||||
if (xmlContent.includes('CrossIndustryInvoice') ||
|
||||
xmlContent.includes('rsm:') ||
|
||||
xmlContent.includes('ram:')) {
|
||||
|
||||
// Check for specific profiles
|
||||
if (xmlContent.includes('factur-x') || xmlContent.includes('Factur-X')) {
|
||||
return interfaces.InvoiceFormat.FACTURX;
|
||||
}
|
||||
if (xmlContent.includes('zugferd') || xmlContent.includes('ZUGFeRD')) {
|
||||
return interfaces.InvoiceFormat.ZUGFERD;
|
||||
}
|
||||
|
||||
return interfaces.InvoiceFormat.CII;
|
||||
}
|
||||
|
||||
// Check for UBL
|
||||
if (xmlContent.includes('<Invoice') ||
|
||||
xmlContent.includes('ubl:Invoice') ||
|
||||
xmlContent.includes('oasis:names:specification:ubl')) {
|
||||
|
||||
// Check for XRechnung
|
||||
if (xmlContent.includes('xrechnung') || xmlContent.includes('XRechnung')) {
|
||||
return interfaces.InvoiceFormat.XRECHNUNG;
|
||||
}
|
||||
|
||||
return interfaces.InvoiceFormat.UBL;
|
||||
}
|
||||
|
||||
// Check for FatturaPA
|
||||
if (xmlContent.includes('FatturaElettronica') ||
|
||||
xmlContent.includes('fatturapa.gov.it')) {
|
||||
return interfaces.InvoiceFormat.FATTURAPA;
|
||||
}
|
||||
|
||||
// For unknown formats, return unknown
|
||||
return interfaces.InvoiceFormat.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user