import { UBLEncoder } from '../generic/ubl.encoder.js'; import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js'; import { DOMParser, XMLSerializer } from '../../../plugins.js'; /** * Encoder for XRechnung (UBL) format * Extends the generic UBL encoder with XRechnung-specific customizations */ export class XRechnungEncoder extends UBLEncoder { /** * Encodes a credit note into XRechnung XML * @param creditNote Credit note to encode * @returns XRechnung XML string */ protected async encodeCreditNote(creditNote: TCreditNote): Promise { // 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 debit note (invoice) into XRechnung XML * @param debitNote Debit note to encode * @returns XRechnung XML string */ protected async encodeDebitNote(debitNote: TDebitNote): Promise { // 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(); } } } } }