fix(compliance): Improve compliance

This commit is contained in:
2025-05-26 10:17:50 +00:00
parent 113ae22c42
commit e7c3a774a3
26 changed files with 2435 additions and 2010 deletions

View File

@ -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));
}
}

View File

@ -34,4 +34,33 @@ export abstract class BaseDecoder {
public getXml(): string {
return this.xml;
}
/**
* Parses a CII date string based on format code
* @param dateStr Date string
* @param format Format code (e.g., '102' for YYYYMMDD)
* @returns Timestamp in milliseconds
*/
protected parseCIIDate(dateStr: string, format?: string): number {
if (!dateStr) return Date.now();
// Format 102 is YYYYMMDD
if (format === '102' && dateStr.length === 8) {
const year = parseInt(dateStr.substring(0, 4));
const month = parseInt(dateStr.substring(4, 6)) - 1; // Month is 0-indexed in JS
const day = parseInt(dateStr.substring(6, 8));
return new Date(year, month, day).getTime();
}
// Format 610 is YYYYMM
if (format === '610' && dateStr.length === 6) {
const year = parseInt(dateStr.substring(0, 4));
const month = parseInt(dateStr.substring(4, 6)) - 1;
return new Date(year, month, 1).getTime();
}
// Try to parse as ISO date or other standard formats
const parsed = Date.parse(dateStr);
return isNaN(parsed) ? Date.now() : parsed;
}
}

View File

@ -41,9 +41,9 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
const typeCode = this.getText('//ram:TypeCode');
if (typeCode === '381') { // Credit note type code
return this.decodeCreditNote();
return this.decodeCreditNote() as unknown as TInvoice;
} else {
return this.decodeDebitNote();
return this.decodeDebitNote() as unknown as TInvoice;
}
}

View File

@ -22,12 +22,8 @@ export abstract class CIIBaseEncoder extends BaseEncoder {
* @returns CII XML string
*/
public async encode(invoice: TInvoice): Promise<string> {
// Determine if it's a credit note or debit note
if (invoice.invoiceType === 'creditnote') {
return this.encodeCreditNote(invoice as TCreditNote);
} else {
return this.encodeDebitNote(invoice as TDebitNote);
}
// TInvoice is always an invoice, treat it as debit note for encoding
return this.encodeDebitNote(invoice as unknown as TDebitNote);
}
/**

View File

@ -18,8 +18,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
// Create a credit note with the common data
return {
...commonData,
invoiceType: 'creditnote'
} as TCreditNote;
accountingDocType: 'creditnote' as const
} as unknown as TCreditNote;
}
/**
@ -33,8 +33,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
// Create a debit note with the common data
return {
...commonData,
invoiceType: 'debitnote'
} as TDebitNote;
accountingDocType: 'debitnote' as const
} as unknown as TDebitNote;
}
/**
@ -47,7 +47,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
// Extract issue date
const issueDateStr = this.getText('//ram:IssueDateTime/udt:DateTimeString');
const issueDate = issueDateStr ? new Date(issueDateStr).getTime() : Date.now();
const issueDateFormat = this.getText('//ram:IssueDateTime/udt:DateTimeString/@format');
const issueDate = this.parseCIIDate(issueDateStr, issueDateFormat);
// Extract seller information
const seller = this.extractParty('//ram:SellerTradeParty');
@ -60,7 +61,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
// Extract due date
const dueDateStr = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString');
const dueDate = dueDateStr ? new Date(dueDateStr).getTime() : Date.now();
const dueDateFormat = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString/@format');
const dueDate = dueDateStr ? this.parseCIIDate(dueDateStr, dueDateFormat) : issueDate;
const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
// Extract currency
@ -77,10 +79,12 @@ export class FacturXDecoder extends CIIBaseDecoder {
// Create the common invoice data
return {
type: 'invoice',
type: 'accounting-doc' as const,
accountingDocType: 'invoice' as const,
id: invoiceId,
accountingDocId: invoiceId,
date: issueDate,
status: 'invoice',
accountingDocStatus: 'issued' as const,
versionInfo: {
type: 'final',
version: '1.0.0'
@ -96,8 +100,7 @@ export class FacturXDecoder extends CIIBaseDecoder {
currency: currencyCode as finance.TCurrency,
notes: notes,
deliveryDate: issueDate,
objectActions: [],
invoiceType: 'debitnote' // Default to debit note, will be overridden in decode methods
objectActions: []
};
}
@ -146,8 +149,8 @@ export class FacturXDecoder extends CIIBaseDecoder {
* Extracts invoice items from Factur-X XML
* @returns Array of invoice items
*/
private extractItems(): finance.TInvoiceItem[] {
const items: finance.TInvoiceItem[] = [];
private extractItems(): finance.TAccountingDocItem[] {
const items: finance.TAccountingDocItem[] = [];
// Get all item nodes
const itemNodes = this.select('//ram:IncludedSupplyChainTradeLineItem', this.doc);

View File

@ -20,7 +20,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
this.setDocumentTypeCode(xmlDoc, '381');
// Add common invoice data
this.addCommonInvoiceData(xmlDoc, creditNote);
this.addCommonInvoiceData(xmlDoc, creditNote as unknown as TInvoice);
// Serialize to string
return new XMLSerializer().serializeToString(xmlDoc);
@ -39,7 +39,7 @@ export class FacturXEncoder extends CIIBaseEncoder {
this.setDocumentTypeCode(xmlDoc, '380');
// Add common invoice data
this.addCommonInvoiceData(xmlDoc, debitNote);
this.addCommonInvoiceData(xmlDoc, debitNote as unknown as TInvoice);
// Serialize to string
return new XMLSerializer().serializeToString(xmlDoc);
@ -145,6 +145,17 @@ export class FacturXEncoder extends CIIBaseEncoder {
issueDateElement.appendChild(dateStringElement);
documentElement.appendChild(issueDateElement);
// Add notes if present
if (invoice.notes && invoice.notes.length > 0) {
for (const note of invoice.notes) {
const noteElement = doc.createElement('ram:IncludedNote');
const contentElement = doc.createElement('ram:Content');
contentElement.textContent = note;
noteElement.appendChild(contentElement);
documentElement.appendChild(noteElement);
}
}
// Create transaction element if it doesn't exist
let transactionElement = root.getElementsByTagName('rsm:SupplyChainTradeTransaction')[0];
if (!transactionElement) {

View File

@ -17,8 +17,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
// Create a credit note with the common data
return {
...commonData,
invoiceType: 'creditnote'
} as TCreditNote;
accountingDocType: 'creditnote' as const
} as unknown as TCreditNote;
}
/**
@ -32,8 +32,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
// Create a debit note with the common data
return {
...commonData,
invoiceType: 'debitnote'
} as TDebitNote;
accountingDocType: 'debitnote' as const
} as unknown as TDebitNote;
}
/**
@ -46,7 +46,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
// Extract issue date
const issueDateStr = this.getText('//ram:IssueDateTime/udt:DateTimeString');
const issueDate = issueDateStr ? new Date(issueDateStr).getTime() : Date.now();
const issueDateFormat = this.getText('//ram:IssueDateTime/udt:DateTimeString/@format');
const issueDate = this.parseCIIDate(issueDateStr, issueDateFormat);
// Extract seller information
const seller = this.extractParty('//ram:SellerTradeParty');
@ -76,10 +77,12 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
// Create the common invoice data
return {
type: 'invoice',
type: 'accounting-doc' as const,
accountingDocType: 'invoice' as const,
id: invoiceId,
accountingDocId: invoiceId,
date: issueDate,
status: 'invoice',
accountingDocStatus: 'issued' as const,
versionInfo: {
type: 'final',
version: '1.0.0'
@ -95,8 +98,7 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
currency: currencyCode as finance.TCurrency,
notes: notes,
deliveryDate: issueDate,
objectActions: [],
invoiceType: 'debitnote' // Default to debit note, will be overridden in decode methods
objectActions: []
};
}
@ -129,7 +131,7 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
houseNumber: houseNumber,
city: city,
postalCode: postalCode,
country: country
countryCode: country
};
// Extract VAT ID
@ -158,8 +160,8 @@ export class ZUGFeRDDecoder extends CIIBaseDecoder {
* Extracts invoice items from ZUGFeRD XML
* @returns Array of invoice items
*/
private extractItems(): finance.TInvoiceItem[] {
const items: finance.TInvoiceItem[] = [];
private extractItems(): finance.TAccountingDocItem[] {
const items: finance.TAccountingDocItem[] = [];
// Get all item nodes
const itemNodes = this.select('//ram:IncludedSupplyChainTradeLineItem', this.doc);

View File

@ -27,7 +27,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
this.setDocumentTypeCode(xmlDoc, '381');
// Add common invoice data
this.addCommonInvoiceData(xmlDoc, creditNote);
this.addCommonInvoiceData(xmlDoc, creditNote as unknown as TInvoice);
// Serialize to string
return new XMLSerializer().serializeToString(xmlDoc);
@ -46,7 +46,7 @@ export class ZUGFeRDEncoder extends CIIBaseEncoder {
this.setDocumentTypeCode(xmlDoc, '380');
// Add common invoice data
this.addCommonInvoiceData(xmlDoc, debitNote);
this.addCommonInvoiceData(xmlDoc, debitNote as unknown as TInvoice);
// Serialize to string
return new XMLSerializer().serializeToString(xmlDoc);

View File

@ -32,8 +32,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
// Create a credit note with the common data
return {
...commonData,
invoiceType: 'creditnote'
} as TCreditNote;
accountingDocType: 'creditnote' as const
} as unknown as TCreditNote;
}
/**
@ -47,8 +47,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
// Create a debit note with the common data
return {
...commonData,
invoiceType: 'debitnote'
} as TDebitNote;
accountingDocType: 'debitnote' as const
} as unknown as TDebitNote;
}
/**
@ -61,7 +61,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
// Extract issue date
const issueDateStr = this.getText('//ram:IssueDateTime/udt:DateTimeString');
const issueDate = issueDateStr ? new Date(issueDateStr).getTime() : Date.now();
const issueDateFormat = this.getText('//ram:IssueDateTime/udt:DateTimeString/@format');
const issueDate = this.parseCIIDate(issueDateStr, issueDateFormat);
// Extract seller information
const seller = this.extractParty('//ram:SellerTradeParty');
@ -91,10 +92,12 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
// Create the common invoice data
return {
type: 'invoice',
type: 'accounting-doc' as const,
accountingDocType: 'invoice' as const,
id: invoiceId,
accountingDocId: invoiceId,
date: issueDate,
status: 'invoice',
accountingDocStatus: 'issued' as const,
versionInfo: {
type: 'final',
version: '1.0.0'
@ -110,8 +113,7 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
currency: currencyCode as finance.TCurrency,
notes: notes,
deliveryDate: issueDate,
objectActions: [],
invoiceType: 'debitnote' // Default to debit note, will be overridden in decode methods
objectActions: []
};
}
@ -144,7 +146,7 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
houseNumber: houseNumber,
city: city,
postalCode: postalCode,
country: country
countryCode: country
};
// Extract VAT ID
@ -173,8 +175,8 @@ export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
* Extracts invoice items from ZUGFeRD v1 XML
* @returns Array of invoice items
*/
private extractItems(): finance.TInvoiceItem[] {
const items: finance.TInvoiceItem[] = [];
private extractItems(): finance.TAccountingDocItem[] {
const items: finance.TAccountingDocItem[] = [];
// Get all item nodes
const itemNodes = this.select('//ram:IncludedSupplyChainTradeLineItem', this.doc);

View File

@ -19,7 +19,7 @@ export class UBLEncoder extends UBLBaseEncoder {
const doc = new DOMParser().parseFromString(xmlString, 'application/xml');
// Add common document elements
this.addCommonElements(doc, creditNote, UBLDocumentType.CREDIT_NOTE);
this.addCommonElements(doc, creditNote as unknown as TInvoice, UBLDocumentType.CREDIT_NOTE);
// Add credit note specific data
this.addCreditNoteSpecificData(doc, creditNote);
@ -39,7 +39,7 @@ export class UBLEncoder extends UBLBaseEncoder {
const doc = new DOMParser().parseFromString(xmlString, 'application/xml');
// Add common document elements
this.addCommonElements(doc, debitNote, UBLDocumentType.INVOICE);
this.addCommonElements(doc, debitNote as unknown as TInvoice, UBLDocumentType.INVOICE);
// Add invoice specific data
this.addInvoiceSpecificData(doc, debitNote);
@ -72,9 +72,10 @@ export class UBLEncoder extends UBLBaseEncoder {
// Issue Date
this.appendElement(doc, root, 'cbc:IssueDate', this.formatDate(invoice.date));
// Due Date
const dueDate = new Date(invoice.date);
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
// Due Date - ensure invoice.date is a valid timestamp
const issueTimestamp = typeof invoice.date === 'number' ? invoice.date : Date.now();
const dueDate = new Date(issueTimestamp);
dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
this.appendElement(doc, root, 'cbc:DueDate', this.formatDate(dueDate.getTime()));
// Document Type Code
@ -258,9 +259,10 @@ export class UBLEncoder extends UBLBaseEncoder {
// Payment means code - default to credit transfer
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentMeansCode', '30');
// Payment due date
const dueDate = new Date(invoice.date);
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
// Payment due date - ensure invoice.date is a valid timestamp
const issueTimestamp = typeof invoice.date === 'number' ? invoice.date : Date.now();
const dueDate = new Date(issueTimestamp);
dueDate.setDate(dueDate.getDate() + (invoice.dueInDays || 30));
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime()));
// Add payment channel code if available

View File

@ -36,9 +36,9 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
const documentType = this.getDocumentType();
if (documentType === UBLDocumentType.CREDIT_NOTE) {
return this.decodeCreditNote();
return this.decodeCreditNote() as unknown as TInvoice;
} else {
return this.decodeDebitNote();
return this.decodeDebitNote() as unknown as TInvoice;
}
}

View File

@ -12,12 +12,8 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
* @returns UBL XML string
*/
public async encode(invoice: TInvoice): Promise<string> {
// Determine if it's a credit note or debit note
if (invoice.invoiceType === 'creditnote') {
return this.encodeCreditNote(invoice as TCreditNote);
} else {
return this.encodeDebitNote(invoice as TDebitNote);
}
// TInvoice is always an invoice, treat it as debit note for encoding
return this.encodeDebitNote(invoice as unknown as TDebitNote);
}
/**
@ -53,7 +49,15 @@ export abstract class UBLBaseEncoder extends BaseEncoder {
* @returns Formatted date string
*/
protected formatDate(timestamp: number): string {
// Ensure timestamp is valid
if (!timestamp || isNaN(timestamp)) {
timestamp = Date.now();
}
const date = new Date(timestamp);
// Check if date is valid
if (isNaN(date.getTime())) {
return new Date().toISOString().split('T')[0];
}
return date.toISOString().split('T')[0];
}
}

View File

@ -19,8 +19,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
// Return the invoice data as a credit note
return {
...commonData,
invoiceType: 'creditnote'
} as TCreditNote;
accountingDocType: 'creditnote' as const
} as unknown as TCreditNote;
}
/**
@ -34,8 +34,8 @@ export class XRechnungDecoder extends UBLBaseDecoder {
// Return the invoice data as a debit note
return {
...commonData,
invoiceType: 'debitnote'
} as TDebitNote;
accountingDocType: 'debitnote' as const
} as unknown as TDebitNote;
}
/**
@ -61,7 +61,7 @@ export class XRechnungDecoder extends UBLBaseDecoder {
}
// Extract items
const items: finance.TInvoiceItem[] = [];
const items: finance.TAccountingDocItem[] = [];
const invoiceLines = this.select('//cac:InvoiceLine', this.doc);
if (invoiceLines && Array.isArray(invoiceLines)) {
@ -121,11 +121,12 @@ export class XRechnungDecoder extends UBLBaseDecoder {
// Create the common invoice data
return {
type: 'invoice',
type: 'accounting-doc' as const,
accountingDocType: 'invoice' as const,
id: invoiceId,
invoiceId: invoiceId,
accountingDocId: invoiceId,
date: issueDate,
status: 'invoice',
accountingDocStatus: 'issued' as const,
versionInfo: {
type: 'final',
version: '1.0.0'
@ -146,11 +147,12 @@ export class XRechnungDecoder extends UBLBaseDecoder {
console.error('Error extracting common data:', error);
// Return default data
return {
type: 'invoice',
type: 'accounting-doc' as const,
accountingDocType: 'invoice' as const,
id: `INV-${Date.now()}`,
invoiceId: `INV-${Date.now()}`,
accountingDocId: `INV-${Date.now()}`,
date: Date.now(),
status: 'invoice',
accountingDocStatus: 'issued' as const,
versionInfo: {
type: 'final',
version: '1.0.0'

View File

@ -1,144 +1,149 @@
import { UBLBaseEncoder } from '../ubl.encoder.js';
import { UBLEncoder } from '../generic/ubl.encoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
import { UBLDocumentType } from '../ubl.types.js';
import { DOMParser, XMLSerializer } from '../../../plugins.js';
/**
* Encoder for XRechnung (UBL) format
* Implements encoding of TInvoice to XRechnung XML
* Extends the generic UBL encoder with XRechnung-specific customizations
*/
export class XRechnungEncoder extends UBLBaseEncoder {
export class XRechnungEncoder extends UBLEncoder {
/**
* Encodes a TCreditNote object to XRechnung XML
* @param creditNote TCreditNote object to encode
* @returns Promise resolving to XML string
* Encodes a credit note into XRechnung XML
* @param creditNote Credit note to encode
* @returns XRechnung XML string
*/
protected async encodeCreditNote(creditNote: TCreditNote): Promise<string> {
// For now, we'll just return a simple UBL credit note template
// In a real implementation, we would generate a proper UBL credit note
return `<?xml version="1.0" encoding="UTF-8"?>
<CreditNote xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</cbc:CustomizationID>
<cbc:ID>${creditNote.id}</cbc:ID>
<cbc:IssueDate>${this.formatDate(creditNote.date)}</cbc:IssueDate>
<cbc:CreditNoteTypeCode>381</cbc:CreditNoteTypeCode>
<cbc:DocumentCurrencyCode>${creditNote.currency}</cbc:DocumentCurrencyCode>
<!-- Rest of the credit note XML would go here -->
</CreditNote>`;
// First get the base UBL XML
const baseXml = await super.encodeCreditNote(creditNote);
// Parse and modify for XRechnung
const doc = new DOMParser().parseFromString(baseXml, 'application/xml');
this.applyXRechnungCustomizations(doc, creditNote as unknown as TInvoice);
// Serialize back to string
return new XMLSerializer().serializeToString(doc);
}
/**
* Encodes a TDebitNote object to XRechnung XML
* @param debitNote TDebitNote object to encode
* @returns Promise resolving to XML string
* Encodes a debit note (invoice) into XRechnung XML
* @param debitNote Debit note to encode
* @returns XRechnung XML string
*/
protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
// For now, we'll just return a simple UBL invoice template
// In a real implementation, we would generate a proper UBL invoice
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</cbc:CustomizationID>
<cbc:ID>${debitNote.id}</cbc:ID>
<cbc:IssueDate>${this.formatDate(debitNote.date)}</cbc:IssueDate>
<cbc:DueDate>${this.formatDate(debitNote.date + debitNote.dueInDays * 24 * 60 * 60 * 1000)}</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>${debitNote.currency}</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>${debitNote.from.name}</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>${debitNote.from.address.streetName || ''}</cbc:StreetName>
<cbc:BuildingNumber>${debitNote.from.address.houseNumber || ''}</cbc:BuildingNumber>
<cbc:CityName>${debitNote.from.address.city || ''}</cbc:CityName>
<cbc:PostalZone>${debitNote.from.address.postalCode || ''}</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>${debitNote.from.address.countryCode || ''}</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
${debitNote.from.registrationDetails?.vatId ? `
<cac:PartyTaxScheme>
<cbc:CompanyID>${debitNote.from.registrationDetails.vatId}</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>` : ''}
${debitNote.from.registrationDetails?.registrationId ? `
<cac:PartyLegalEntity>
<cbc:RegistrationName>${debitNote.from.registrationDetails.registrationName || debitNote.from.name}</cbc:RegistrationName>
<cbc:CompanyID>${debitNote.from.registrationDetails.registrationId}</cbc:CompanyID>
</cac:PartyLegalEntity>` : ''}
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>${debitNote.to.name}</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>${debitNote.to.address.streetName || ''}</cbc:StreetName>
<cbc:BuildingNumber>${debitNote.to.address.houseNumber || ''}</cbc:BuildingNumber>
<cbc:CityName>${debitNote.to.address.city || ''}</cbc:CityName>
<cbc:PostalZone>${debitNote.to.address.postalCode || ''}</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>${debitNote.to.address.countryCode || ''}</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
${debitNote.to.registrationDetails?.vatId ? `
<cac:PartyTaxScheme>
<cbc:CompanyID>${debitNote.to.registrationDetails.vatId}</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>` : ''}
</cac:Party>
</cac:AccountingCustomerParty>
<cac:PaymentTerms>
<cbc:Note>Due in ${debitNote.dueInDays} days</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="${debitNote.currency}">0.00</cbc:TaxAmount>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="${debitNote.currency}">0.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="${debitNote.currency}">0.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="${debitNote.currency}">0.00</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="${debitNote.currency}">0.00</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
${debitNote.items.map((item, index) => `
<cac:InvoiceLine>
<cbc:ID>${index + 1}</cbc:ID>
<cbc:InvoicedQuantity unitCode="${item.unitType}">${item.unitQuantity}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="${debitNote.currency}">${item.unitNetPrice * item.unitQuantity}</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>${item.name}</cbc:Name>
${item.articleNumber ? `
<cac:SellersItemIdentification>
<cbc:ID>${item.articleNumber}</cbc:ID>
</cac:SellersItemIdentification>` : ''}
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>${item.vatPercentage}</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="${debitNote.currency}">${item.unitNetPrice}</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>`).join('')}
</Invoice>`;
// First get the base UBL XML
const baseXml = await super.encodeDebitNote(debitNote);
// Parse and modify for XRechnung
const doc = new DOMParser().parseFromString(baseXml, 'application/xml');
this.applyXRechnungCustomizations(doc, debitNote as unknown as TInvoice);
// Serialize back to string
return new XMLSerializer().serializeToString(doc);
}
}
/**
* Applies XRechnung-specific customizations to the document
* @param doc XML document
* @param invoice Invoice data
*/
private applyXRechnungCustomizations(doc: Document, invoice: TInvoice): void {
const root = doc.documentElement;
// Update Customization ID to XRechnung 2.0
const customizationId = root.getElementsByTagName('cbc:CustomizationID')[0];
if (customizationId) {
customizationId.textContent = 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0';
}
// Add or update Buyer Reference (required for XRechnung)
let buyerRef = root.getElementsByTagName('cbc:BuyerReference')[0];
if (!buyerRef) {
// Find where to insert it (after DocumentCurrencyCode)
const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
if (currencyCode) {
buyerRef = doc.createElement('cbc:BuyerReference');
buyerRef.textContent = invoice.buyerReference || invoice.id;
currencyCode.parentNode!.insertBefore(buyerRef, currencyCode.nextSibling);
}
} else if (!buyerRef.textContent || buyerRef.textContent.trim() === '') {
buyerRef.textContent = invoice.buyerReference || invoice.id;
}
// Update payment terms to German
const paymentTermsNotes = root.getElementsByTagName('cac:PaymentTerms');
if (paymentTermsNotes.length > 0) {
const noteElement = paymentTermsNotes[0].getElementsByTagName('cbc:Note')[0];
if (noteElement && noteElement.textContent) {
noteElement.textContent = `Zahlung innerhalb von ${invoice.dueInDays || 30} Tagen`;
}
}
// Add electronic address for parties if available
this.addElectronicAddressToParty(doc, 'cac:AccountingSupplierParty', invoice.from);
this.addElectronicAddressToParty(doc, 'cac:AccountingCustomerParty', invoice.to);
// Ensure payment reference is set
const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
if (paymentMeans) {
let paymentId = paymentMeans.getElementsByTagName('cbc:PaymentID')[0];
if (!paymentId) {
paymentId = doc.createElement('cbc:PaymentID');
paymentId.textContent = invoice.id;
paymentMeans.appendChild(paymentId);
}
}
// Add country code handling for German addresses
this.fixGermanCountryCodes(doc);
}
/**
* Adds electronic address to party if not already present
* @param doc XML document
* @param partyType Party type selector
* @param party Party data
*/
private addElectronicAddressToParty(doc: Document, partyType: string, party: any): void {
const partyContainer = doc.getElementsByTagName(partyType)[0];
if (!partyContainer) return;
const partyElement = partyContainer.getElementsByTagName('cac:Party')[0];
if (!partyElement) return;
// Check if electronic address already exists
const existingEndpoint = partyElement.getElementsByTagName('cbc:EndpointID')[0];
if (!existingEndpoint && party.electronicAddress) {
// Add electronic address at the beginning of party element
const endpointNode = doc.createElement('cbc:EndpointID');
endpointNode.setAttribute('schemeID', party.electronicAddress.scheme || '0204');
endpointNode.textContent = party.electronicAddress.value;
// Insert as first child of party element
if (partyElement.firstChild) {
partyElement.insertBefore(endpointNode, partyElement.firstChild);
} else {
partyElement.appendChild(endpointNode);
}
}
}
/**
* Fixes German country codes in the document
* @param doc XML document
*/
private fixGermanCountryCodes(doc: Document): void {
const countryNodes = doc.getElementsByTagName('cbc:IdentificationCode');
for (let i = 0; i < countryNodes.length; i++) {
const node = countryNodes[i];
if (node.textContent) {
const text = node.textContent.toLowerCase();
if (text === 'germany' || text === 'deutschland' || text === 'de') {
node.textContent = 'DE';
} else if (text.length > 2) {
// Try to use first 2 characters as country code
node.textContent = text.substring(0, 2).toUpperCase();
}
}
}
}
}

View File

@ -51,7 +51,7 @@ export enum InvoiceFormat {
* This is a subset of InvoiceFormat that only includes formats
* that can be generated and embedded in PDFs
*/
export type ExportFormat = 'facturx' | 'zugferd' | 'xrechnung' | 'ubl';
export type ExportFormat = 'facturx' | 'zugferd' | 'xrechnung' | 'ubl' | 'cii';
/**
* Describes a validation level for invoice validation

View File

@ -18,7 +18,7 @@ export enum InvoiceFormat {
* This is a subset of InvoiceFormat that only includes formats
* that can be generated and embedded in PDFs
*/
export type ExportFormat = 'facturx' | 'zugferd' | 'xrechnung' | 'ubl';
export type ExportFormat = 'facturx' | 'zugferd' | 'xrechnung' | 'ubl' | 'cii';
/**
* Describes a validation level for invoice validation

View File

@ -4,6 +4,11 @@
* to make the codebase more maintainable and follow the DRY principle.
*/
// Node.js built-in modules
import * as fs from 'fs/promises';
import * as path from 'path';
import * as crypto from 'crypto';
// PDF-related imports
import {
PDFDocument,
@ -27,6 +32,11 @@ import { business, finance, general } from '@tsclass/tsclass';
// Re-export all imports
export {
// Node.js built-in modules
fs,
path,
crypto,
// PDF-lib exports
PDFDocument,
PDFDict,