fix(compliance): Improve compliance
This commit is contained in:
760
ts/einvoice.ts
760
ts/einvoice.ts
@ -1,7 +1,7 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
import { business, finance } from './plugins.js';
|
||||
import type { TInvoice } from './interfaces/common.js';
|
||||
import type { TInvoice, TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
||||
import { InvoiceFormat, ValidationLevel } from './interfaces/common.js';
|
||||
import type { ValidationResult, ValidationError, EInvoiceOptions, IPdf, ExportFormat } from './interfaces/common.js';
|
||||
|
||||
@ -30,34 +30,84 @@ 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
|
||||
* Extends the TInvoice interface for seamless integration with existing systems
|
||||
*/
|
||||
export class EInvoice {
|
||||
// TInvoice interface properties
|
||||
export class EInvoice implements TInvoice {
|
||||
/**
|
||||
* Creates an EInvoice instance from XML string
|
||||
* @param xmlString XML string to parse
|
||||
* @returns EInvoice instance
|
||||
*/
|
||||
public static async fromXml(xmlString: string): Promise<EInvoice> {
|
||||
const invoice = new EInvoice();
|
||||
await invoice.fromXmlString(xmlString);
|
||||
return invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an EInvoice instance from file
|
||||
* @param filePath Path to the file
|
||||
* @returns EInvoice instance
|
||||
*/
|
||||
public static async fromFile(filePath: string): Promise<EInvoice> {
|
||||
const invoice = new EInvoice();
|
||||
await invoice.fromFile(filePath);
|
||||
return invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an EInvoice instance from PDF
|
||||
* @param pdfBuffer PDF buffer
|
||||
* @returns EInvoice instance
|
||||
*/
|
||||
public static async fromPdf(pdfBuffer: Buffer | string): Promise<EInvoice> {
|
||||
const invoice = new EInvoice();
|
||||
if (typeof pdfBuffer === 'string') {
|
||||
// If given a file path
|
||||
await invoice.fromPdfFile(pdfBuffer);
|
||||
} else {
|
||||
// If given a buffer, extract XML and parse it
|
||||
const extractResult = await invoice.pdfExtractor.extractXml(pdfBuffer);
|
||||
if (!extractResult.success || !extractResult.xml) {
|
||||
throw new EInvoicePDFError('No invoice XML found in PDF', 'extract');
|
||||
}
|
||||
await invoice.fromXmlString(extractResult.xml);
|
||||
}
|
||||
return invoice;
|
||||
}
|
||||
|
||||
// TInvoice interface properties - accounting document structure
|
||||
public type: 'accounting-doc' = 'accounting-doc';
|
||||
public accountingDocType: 'invoice' = 'invoice';
|
||||
public accountingDocId: string = '';
|
||||
public accountingDocStatus: 'draft' | 'issued' | 'paid' | 'canceled' | 'refunded' = 'issued';
|
||||
|
||||
// Business envelope properties
|
||||
public id: string = '';
|
||||
public invoiceId: string = '';
|
||||
public invoiceType: 'creditnote' | 'debitnote' = 'debitnote';
|
||||
public date = Date.now();
|
||||
public status: 'draft' | 'issued' | 'paid' | 'canceled' | 'refunded' = 'issued';
|
||||
public subject: string = '';
|
||||
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 = '';
|
||||
|
||||
// Contact information
|
||||
public from: business.TContact;
|
||||
public to: business.TContact;
|
||||
public legalContact?: business.TContact;
|
||||
|
||||
// Additional envelope properties
|
||||
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[] = [];
|
||||
// Accounting document specific properties
|
||||
public items: TAccountingDocItem[] = [];
|
||||
public dueInDays: number = 30;
|
||||
public reverseCharge: boolean = false;
|
||||
public currency: finance.TCurrency = 'EUR';
|
||||
@ -67,8 +117,66 @@ export class EInvoice {
|
||||
public buyerReference?: string;
|
||||
public electronicAddress?: { scheme: string; value: string };
|
||||
public paymentOptions?: finance.IPaymentOptionInfo;
|
||||
public relatedDocuments?: Array<{
|
||||
relationType: 'corrects' | 'replaces' | 'references';
|
||||
documentId: string;
|
||||
issueDate?: number;
|
||||
}>;
|
||||
public printResult?: {
|
||||
pdfBufferString: string;
|
||||
totalNet: number;
|
||||
totalGross: number;
|
||||
vatGroups: {
|
||||
percentage: number;
|
||||
items: TAccountingDocItem[];
|
||||
}[];
|
||||
};
|
||||
|
||||
// Backward compatibility properties
|
||||
public get invoiceId(): string { return this.accountingDocId; }
|
||||
public set invoiceId(value: string) { this.accountingDocId = value; }
|
||||
|
||||
public get invoiceType(): 'invoice' | 'creditnote' | 'debitnote' {
|
||||
return this.accountingDocType === 'invoice' ? 'invoice' :
|
||||
this.accountingDocType === 'creditnote' ? 'creditnote' : 'debitnote';
|
||||
}
|
||||
public set invoiceType(value: 'invoice' | 'creditnote' | 'debitnote') {
|
||||
this.accountingDocType = 'invoice'; // Always set to invoice for TInvoice type
|
||||
}
|
||||
|
||||
// Computed properties for convenience
|
||||
public get issueDate(): Date {
|
||||
return new Date(this.date);
|
||||
}
|
||||
public set issueDate(value: Date) {
|
||||
this.date = value.getTime();
|
||||
}
|
||||
|
||||
public get totalNet(): number {
|
||||
return this.calculateTotalNet();
|
||||
}
|
||||
|
||||
public get totalVat(): number {
|
||||
return this.calculateTotalVat();
|
||||
}
|
||||
|
||||
public get totalGross(): number {
|
||||
return this.totalNet + this.totalVat;
|
||||
}
|
||||
|
||||
public get taxBreakdown(): Array<{ taxPercent: number; netAmount: number; taxAmount: number }> {
|
||||
return this.calculateTaxBreakdown();
|
||||
}
|
||||
|
||||
// EInvoice specific properties
|
||||
public metadata?: {
|
||||
format?: InvoiceFormat;
|
||||
version?: string;
|
||||
profile?: string;
|
||||
customizationId?: string;
|
||||
extensions?: Record<string, any>;
|
||||
};
|
||||
|
||||
private xmlString: string = '';
|
||||
private detectedFormat: InvoiceFormat = InvoiceFormat.UNKNOWN;
|
||||
private validationErrors: ValidationError[] = [];
|
||||
@ -101,263 +209,289 @@ export class EInvoice {
|
||||
*/
|
||||
private createEmptyContact(): business.TContact {
|
||||
return {
|
||||
name: '',
|
||||
type: 'company',
|
||||
name: '',
|
||||
description: '',
|
||||
address: {
|
||||
streetName: '',
|
||||
houseNumber: '0',
|
||||
houseNumber: '',
|
||||
city: '',
|
||||
country: '',
|
||||
postalCode: ''
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: {
|
||||
year: 2000,
|
||||
month: 1,
|
||||
day: 1
|
||||
postalCode: '',
|
||||
country: ''
|
||||
},
|
||||
registrationDetails: {
|
||||
vatId: '',
|
||||
registrationId: '',
|
||||
registrationName: ''
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: {
|
||||
year: new Date().getFullYear(),
|
||||
month: new Date().getMonth() + 1,
|
||||
day: new Date().getDate()
|
||||
}
|
||||
} as business.TCompany;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the invoice as XML in the specified format
|
||||
* @param format The export format
|
||||
* @returns XML string
|
||||
*/
|
||||
public async exportXml(format: ExportFormat): Promise<string> {
|
||||
return this.toXmlString(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads invoice data from XML (alias for fromXmlString)
|
||||
* @param xmlString The XML string to parse
|
||||
* @returns The EInvoice instance for chaining
|
||||
*/
|
||||
public async loadXml(xmlString: string): Promise<EInvoice> {
|
||||
return this.fromXmlString(xmlString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads invoice data from an XML string
|
||||
* @param xmlString The XML string to parse
|
||||
* @returns The EInvoice instance for chaining
|
||||
*/
|
||||
public async fromXmlString(xmlString: string): Promise<EInvoice> {
|
||||
try {
|
||||
this.xmlString = xmlString;
|
||||
|
||||
// Detect format
|
||||
this.detectedFormat = FormatDetector.detectFormat(xmlString);
|
||||
if (this.detectedFormat === InvoiceFormat.UNKNOWN) {
|
||||
throw new EInvoiceFormatError('Unknown invoice format', { sourceFormat: 'unknown' });
|
||||
}
|
||||
|
||||
// Get appropriate decoder
|
||||
const decoder = DecoderFactory.createDecoder(xmlString);
|
||||
const invoice = await decoder.decode();
|
||||
|
||||
// Map the decoded invoice to our properties
|
||||
this.mapFromTInvoice(invoice);
|
||||
|
||||
// Validate if requested
|
||||
if (this.options.validateOnLoad) {
|
||||
await this.validate(this.options.validationLevel);
|
||||
}
|
||||
|
||||
return this;
|
||||
} catch (error) {
|
||||
if (error instanceof EInvoiceError) {
|
||||
throw error;
|
||||
}
|
||||
throw new EInvoiceParsingError(`Failed to parse XML: ${error.message}`, {}, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads invoice data from a file
|
||||
* @param filePath Path to the file to load
|
||||
* @returns The EInvoice instance for chaining
|
||||
*/
|
||||
public async fromFile(filePath: string): Promise<EInvoice> {
|
||||
try {
|
||||
const fileBuffer = await plugins.fs.readFile(filePath);
|
||||
|
||||
// Check if it's a PDF
|
||||
if (filePath.toLowerCase().endsWith('.pdf') || fileBuffer.subarray(0, 4).toString() === '%PDF') {
|
||||
return this.fromPdfFile(filePath);
|
||||
}
|
||||
|
||||
// Otherwise treat as XML
|
||||
const xmlString = fileBuffer.toString('utf-8');
|
||||
return this.fromXmlString(xmlString);
|
||||
} catch (error) {
|
||||
throw new EInvoiceError(`Failed to load file: ${error.message}`, 'FILE_LOAD_ERROR', { filePath });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads invoice data from a PDF file
|
||||
* @param filePath Path to the PDF file
|
||||
* @returns The EInvoice instance for chaining
|
||||
*/
|
||||
public async fromPdfFile(filePath: string): Promise<EInvoice> {
|
||||
try {
|
||||
const pdfBuffer = await plugins.fs.readFile(filePath);
|
||||
const extractResult = await this.pdfExtractor.extractXml(pdfBuffer);
|
||||
const extractedXml = extractResult.success ? extractResult.xml : null;
|
||||
|
||||
if (!extractedXml) {
|
||||
throw new EInvoicePDFError('No invoice XML found in PDF', 'extract', { filePath });
|
||||
}
|
||||
|
||||
// Store the PDF for later use
|
||||
this.pdf = {
|
||||
name: plugins.path.basename(filePath),
|
||||
id: plugins.crypto.createHash('md5').update(pdfBuffer).digest('hex'),
|
||||
buffer: new Uint8Array(pdfBuffer),
|
||||
metadata: {
|
||||
textExtraction: '',
|
||||
format: 'PDF/A-3',
|
||||
embeddedXml: {
|
||||
filename: 'factur-x.xml',
|
||||
description: 'Factur-X Invoice'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return this.fromXmlString(extractedXml);
|
||||
} catch (error) {
|
||||
if (error instanceof EInvoiceError) {
|
||||
throw error;
|
||||
}
|
||||
throw new EInvoicePDFError(`Failed to extract invoice from PDF: ${error.message}`, 'extract', {}, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps data from a TInvoice to this EInvoice instance
|
||||
*/
|
||||
private mapFromTInvoice(invoice: TInvoice): void {
|
||||
// Map all properties from the decoded invoice
|
||||
Object.assign(this, invoice);
|
||||
|
||||
// Ensure backward compatibility
|
||||
if (!this.id && this.accountingDocId) {
|
||||
this.id = this.accountingDocId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps this EInvoice instance to a TInvoice
|
||||
*/
|
||||
private mapToTInvoice(): TInvoice {
|
||||
return {
|
||||
type: 'accounting-doc',
|
||||
accountingDocType: this.accountingDocType,
|
||||
accountingDocId: this.accountingDocId || this.id,
|
||||
accountingDocStatus: this.accountingDocStatus,
|
||||
id: this.id,
|
||||
date: this.date,
|
||||
status: this.status,
|
||||
subject: this.subject,
|
||||
versionInfo: this.versionInfo,
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
legalContact: this.legalContact,
|
||||
incidenceId: this.incidenceId,
|
||||
language: this.language,
|
||||
objectActions: this.objectActions,
|
||||
items: this.items,
|
||||
dueInDays: this.dueInDays,
|
||||
reverseCharge: this.reverseCharge,
|
||||
currency: this.currency,
|
||||
notes: this.notes,
|
||||
periodOfPerformance: this.periodOfPerformance,
|
||||
deliveryDate: this.deliveryDate,
|
||||
buyerReference: this.buyerReference,
|
||||
electronicAddress: this.electronicAddress,
|
||||
paymentOptions: this.paymentOptions,
|
||||
relatedDocuments: this.relatedDocuments,
|
||||
printResult: this.printResult
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new EInvoice instance from XML
|
||||
* @param xmlString XML content
|
||||
* @param options Configuration options
|
||||
* @returns EInvoice instance
|
||||
* Exports the invoice to an XML string in the specified format
|
||||
* @param format The target format
|
||||
* @returns The XML string
|
||||
*/
|
||||
public static async fromXml(xmlString: string, options?: EInvoiceOptions): Promise<EInvoice> {
|
||||
const einvoice = new EInvoice(options);
|
||||
|
||||
// Load XML data
|
||||
await einvoice.loadXml(xmlString);
|
||||
|
||||
return einvoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new EInvoice instance from PDF
|
||||
* @param pdfBuffer PDF buffer
|
||||
* @param options Configuration options
|
||||
* @returns EInvoice instance
|
||||
*/
|
||||
public static async fromPdf(pdfBuffer: Uint8Array | Buffer, options?: EInvoiceOptions): Promise<EInvoice> {
|
||||
const einvoice = new EInvoice(options);
|
||||
|
||||
// Load PDF data
|
||||
await einvoice.loadPdf(pdfBuffer);
|
||||
|
||||
return einvoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<EInvoice> {
|
||||
this.xmlString = xmlString;
|
||||
|
||||
// Detect format
|
||||
this.detectedFormat = FormatDetector.detectFormat(xmlString);
|
||||
|
||||
public async toXmlString(format: ExportFormat): Promise<string> {
|
||||
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);
|
||||
}
|
||||
const encoder = EncoderFactory.createEncoder(format);
|
||||
const invoice = this.mapToTInvoice();
|
||||
return await encoder.encode(invoice);
|
||||
} catch (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);
|
||||
throw new EInvoiceFormatError(`Failed to encode to ${format}: ${error.message}`, { targetFormat: format });
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads PDF data into the EInvoice instance
|
||||
* @param pdfBuffer PDF buffer
|
||||
* @param validate Whether to validate the extracted XML
|
||||
* @returns This instance for chaining
|
||||
* Validates the invoice
|
||||
* @param level The validation level to use
|
||||
* @returns The validation result
|
||||
*/
|
||||
public async loadPdf(pdfBuffer: Uint8Array | Buffer, validate: boolean = false): Promise<EInvoice> {
|
||||
public async validate(level: ValidationLevel = ValidationLevel.BUSINESS): Promise<ValidationResult> {
|
||||
try {
|
||||
// Extract XML from PDF using the consolidated extractor
|
||||
const extractResult = await this.pdfExtractor.extractXml(pdfBuffer);
|
||||
|
||||
// Store the PDF buffer
|
||||
this.pdf = {
|
||||
name: 'invoice.pdf',
|
||||
id: `invoice-${Date.now()}`,
|
||||
metadata: {
|
||||
textExtraction: '',
|
||||
format: extractResult.success ? extractResult.format?.toString() : undefined
|
||||
},
|
||||
buffer: pdfBuffer instanceof Buffer ? new Uint8Array(pdfBuffer) : pdfBuffer
|
||||
};
|
||||
|
||||
// Handle extraction result
|
||||
if (!extractResult.success || !extractResult.xml) {
|
||||
const errorMessage = extractResult.error ? extractResult.error.message : 'Unknown error extracting XML from PDF';
|
||||
throw new EInvoicePDFError(
|
||||
`Failed to extract XML from PDF: ${errorMessage}`,
|
||||
'extract',
|
||||
{
|
||||
pdfInfo: {
|
||||
size: pdfBuffer.length,
|
||||
filename: 'invoice.pdf'
|
||||
},
|
||||
extractionMethod: 'standard'
|
||||
},
|
||||
extractResult.error?.originalError
|
||||
);
|
||||
const format = this.detectedFormat || InvoiceFormat.UNKNOWN;
|
||||
if (format === InvoiceFormat.UNKNOWN) {
|
||||
throw new EInvoiceValidationError('Cannot validate: format unknown', []);
|
||||
}
|
||||
|
||||
// Load the extracted XML
|
||||
await this.loadXml(extractResult.xml, validate);
|
||||
|
||||
// Store the detected format
|
||||
this.detectedFormat = extractResult.format || InvoiceFormat.UNKNOWN;
|
||||
|
||||
return this;
|
||||
} catch (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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 EInvoiceValidationError(
|
||||
'No XML content available for validation',
|
||||
[{
|
||||
code: 'VAL-001',
|
||||
message: 'XML content must be loaded before validation',
|
||||
severity: 'error'
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
const validationError = new EInvoiceValidationError(
|
||||
`Validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
[{
|
||||
code: 'VAL-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
|
||||
};
|
||||
if (error instanceof EInvoiceError) {
|
||||
throw error;
|
||||
}
|
||||
throw new EInvoiceValidationError(`Validation failed: ${error.message}`, [], { validationLevel: level });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the invoice is valid
|
||||
* @returns True if no validation errors were found
|
||||
* Embeds the invoice XML into a PDF
|
||||
* @param pdfBuffer The PDF buffer to embed into
|
||||
* @param format The format to use for embedding
|
||||
* @returns The PDF buffer with embedded XML
|
||||
*/
|
||||
public isValid(): boolean {
|
||||
return this.validationErrors.length === 0;
|
||||
public async embedInPdf(pdfBuffer: Buffer, format: ExportFormat = 'facturx'): Promise<Buffer> {
|
||||
try {
|
||||
const xmlString = await this.toXmlString(format);
|
||||
const embedResult = await this.pdfEmbedder.embedXml(pdfBuffer, xmlString, 'invoice.xml', `${format} Invoice`);
|
||||
if (!embedResult.success) {
|
||||
throw new EInvoicePDFError('Failed to embed XML in PDF', 'embed', { format });
|
||||
}
|
||||
return embedResult.data! as Buffer;
|
||||
} catch (error) {
|
||||
throw new EInvoicePDFError(`Failed to embed XML in PDF: ${error.message}`, 'embed', { format }, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets validation errors from the last validation
|
||||
* Saves the invoice to a file
|
||||
* @param filePath The path to save to
|
||||
* @param format The format to save in
|
||||
*/
|
||||
public async saveToFile(filePath: string, format?: ExportFormat): Promise<void> {
|
||||
try {
|
||||
// Determine format from file extension if not provided
|
||||
if (!format && filePath.toLowerCase().endsWith('.xml')) {
|
||||
format = this.detectedFormat === InvoiceFormat.UBL ? 'ubl' :
|
||||
this.detectedFormat === InvoiceFormat.ZUGFERD ? 'zugferd' :
|
||||
this.detectedFormat === InvoiceFormat.FACTURX ? 'facturx' :
|
||||
'xrechnung';
|
||||
}
|
||||
|
||||
if (filePath.toLowerCase().endsWith('.pdf')) {
|
||||
// Save as PDF with embedded XML
|
||||
if (!this.pdf) {
|
||||
throw new EInvoiceError('No PDF available to save', 'NO_PDF_ERROR');
|
||||
}
|
||||
const pdfWithXml = await this.embedInPdf(Buffer.from(this.pdf.buffer), format);
|
||||
await plugins.fs.writeFile(filePath, pdfWithXml);
|
||||
} else {
|
||||
// Save as XML
|
||||
const xmlString = await this.toXmlString(format || 'xrechnung');
|
||||
await plugins.fs.writeFile(filePath, xmlString, 'utf-8');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof EInvoiceError) {
|
||||
throw error;
|
||||
}
|
||||
throw new EInvoiceError(`Failed to save file: ${error.message}`, 'FILE_SAVE_ERROR', { filePath });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the validation errors
|
||||
* @returns Array of validation errors
|
||||
*/
|
||||
public getValidationErrors(): ValidationError[] {
|
||||
@ -365,113 +499,83 @@ export class EInvoice {
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the invoice as XML in the specified format
|
||||
* @param format Target format (e.g., 'facturx', 'xrechnung')
|
||||
* @returns XML string in the specified format
|
||||
* Checks if the invoice is valid
|
||||
* @returns True if valid, false otherwise
|
||||
*/
|
||||
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);
|
||||
public isValid(): boolean {
|
||||
return this.validationErrors.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
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 result = await this.pdfEmbedder.createPdfWithXml(
|
||||
this.pdf.buffer,
|
||||
xmlContent,
|
||||
filename,
|
||||
description,
|
||||
this.pdf.name,
|
||||
this.pdf.id
|
||||
);
|
||||
|
||||
// Handle potential errors
|
||||
if (!result.success || !result.pdf) {
|
||||
const errorMessage = result.error ? result.error.message : 'Unknown error embedding XML into PDF';
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Gets the detected format
|
||||
* @returns The detected invoice format
|
||||
*/
|
||||
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
|
||||
* Calculates the total net amount
|
||||
*/
|
||||
public isFormat(format: InvoiceFormat): boolean {
|
||||
return this.detectedFormat === format;
|
||||
private calculateTotalNet(): number {
|
||||
return this.items.reduce((sum, item) => {
|
||||
return sum + (item.unitQuantity * item.unitNetPrice);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total VAT amount
|
||||
*/
|
||||
private calculateTotalVat(): number {
|
||||
return this.items.reduce((sum, item) => {
|
||||
const net = item.unitQuantity * item.unitNetPrice;
|
||||
return sum + (net * item.vatPercentage / 100);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates tax breakdown by rate
|
||||
*/
|
||||
private calculateTaxBreakdown(): Array<{ taxPercent: number; netAmount: number; taxAmount: number }> {
|
||||
const breakdown = new Map<number, { net: number; tax: number }>();
|
||||
|
||||
this.items.forEach(item => {
|
||||
const net = item.unitQuantity * item.unitNetPrice;
|
||||
const tax = net * item.vatPercentage / 100;
|
||||
|
||||
const current = breakdown.get(item.vatPercentage) || { net: 0, tax: 0 };
|
||||
breakdown.set(item.vatPercentage, {
|
||||
net: current.net + net,
|
||||
tax: current.tax + tax
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(breakdown.entries()).map(([rate, amounts]) => ({
|
||||
taxPercent: rate,
|
||||
netAmount: amounts.net,
|
||||
taxAmount: amounts.tax
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new invoice item
|
||||
*/
|
||||
public createItem(data: Partial<TAccountingDocItem>): TAccountingDocItem {
|
||||
return {
|
||||
position: data.position || this.items.length + 1,
|
||||
name: data.name || '',
|
||||
articleNumber: data.articleNumber,
|
||||
unitType: data.unitType || 'unit',
|
||||
unitQuantity: data.unitQuantity || 1,
|
||||
unitNetPrice: data.unitNetPrice || 0,
|
||||
vatPercentage: data.vatPercentage || 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to the invoice
|
||||
*/
|
||||
public addItem(item: Partial<TAccountingDocItem>): void {
|
||||
this.items.push(this.createItem(item));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user