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

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