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; // Extract metadata if available const metadata = (invoice as any).metadata?.extensions; // 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]; const buyerReferenceValue = (invoice as any).buyerReference || metadata?.businessReferences?.buyerReference || invoice.id; 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 = buyerReferenceValue; currencyCode.parentNode!.insertBefore(buyerRef, currencyCode.nextSibling); } } else if (!buyerRef.textContent || buyerRef.textContent.trim() === '') { buyerRef.textContent = buyerReferenceValue; } // 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); // Preserve business references from metadata this.addBusinessReferences(doc, metadata?.businessReferences); // Preserve payment information from metadata this.enhancePaymentInformation(doc, metadata?.paymentInformation); // Preserve date information from metadata this.addDateInformation(doc, metadata?.dateInformation); // Enhance party information with contact details this.enhancePartyInformation(doc, invoice); // Enhance line items with metadata this.enhanceLineItems(doc, invoice); } /** * 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); } } // Add GLN (Global Location Number) if available if (party.gln && !existingEndpoint) { const endpointNode = doc.createElement('cbc:EndpointID'); endpointNode.setAttribute('schemeID', '0088'); // GLN scheme ID endpointNode.textContent = party.gln; // Insert as first child of party element if (partyElement.firstChild) { partyElement.insertBefore(endpointNode, partyElement.firstChild); } else { partyElement.appendChild(endpointNode); } } // Add PartyIdentification for additional identifiers if (party.additionalIdentifiers) { for (const identifier of party.additionalIdentifiers) { const partyId = doc.createElement('cac:PartyIdentification'); const id = doc.createElement('cbc:ID'); if (identifier.scheme) { id.setAttribute('schemeID', identifier.scheme); } id.textContent = identifier.value; partyId.appendChild(id); // Insert after EndpointID or at beginning const endpoint = partyElement.getElementsByTagName('cbc:EndpointID')[0]; if (endpoint && endpoint.nextSibling) { partyElement.insertBefore(partyId, endpoint.nextSibling); } else if (partyElement.firstChild) { partyElement.insertBefore(partyId, partyElement.firstChild); } else { partyElement.appendChild(partyId); } } } // Add company registration number to PartyLegalEntity if (party.registrationDetails?.registrationId) { let legalEntity = partyElement.getElementsByTagName('cac:PartyLegalEntity')[0]; if (!legalEntity) { legalEntity = doc.createElement('cac:PartyLegalEntity'); // Insert after PostalAddress const postalAddress = partyElement.getElementsByTagName('cac:PostalAddress')[0]; if (postalAddress && postalAddress.nextSibling) { partyElement.insertBefore(legalEntity, postalAddress.nextSibling); } else { partyElement.appendChild(legalEntity); } } // Add registration name if not present if (!legalEntity.getElementsByTagName('cbc:RegistrationName')[0]) { const regName = doc.createElement('cbc:RegistrationName'); regName.textContent = party.registrationDetails.registrationName || party.name; legalEntity.appendChild(regName); } // Add company ID if not present if (!legalEntity.getElementsByTagName('cbc:CompanyID')[0]) { const companyId = doc.createElement('cbc:CompanyID'); companyId.textContent = party.registrationDetails.registrationId; legalEntity.appendChild(companyId); } } } /** * 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(); } } } } /** * Adds business references from metadata to the document * @param doc XML document * @param businessReferences Business references from metadata */ private addBusinessReferences(doc: Document, businessReferences?: any): void { if (!businessReferences) return; const root = doc.documentElement; // Add OrderReference if (businessReferences.orderReference && !root.getElementsByTagName('cac:OrderReference')[0]) { const orderRef = doc.createElement('cac:OrderReference'); const orderId = doc.createElement('cbc:ID'); orderId.textContent = businessReferences.orderReference; orderRef.appendChild(orderId); // Insert after DocumentCurrencyCode const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0]; if (currencyCode && currencyCode.parentNode) { currencyCode.parentNode.insertBefore(orderRef, currencyCode.nextSibling); } } // Add ContractDocumentReference if (businessReferences.contractReference && !root.getElementsByTagName('cac:ContractDocumentReference')[0]) { const contractRef = doc.createElement('cac:ContractDocumentReference'); const contractId = doc.createElement('cbc:ID'); contractId.textContent = businessReferences.contractReference; contractRef.appendChild(contractId); // Insert after OrderReference or DocumentCurrencyCode const orderRef = root.getElementsByTagName('cac:OrderReference')[0]; const insertAfter = orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0]; if (insertAfter && insertAfter.parentNode) { insertAfter.parentNode.insertBefore(contractRef, insertAfter.nextSibling); } } // Add ProjectReference if (businessReferences.projectReference && !root.getElementsByTagName('cac:ProjectReference')[0]) { const projectRef = doc.createElement('cac:ProjectReference'); const projectId = doc.createElement('cbc:ID'); projectId.textContent = businessReferences.projectReference; projectRef.appendChild(projectId); // Insert after ContractDocumentReference or other refs const contractRef = root.getElementsByTagName('cac:ContractDocumentReference')[0]; const orderRef = root.getElementsByTagName('cac:OrderReference')[0]; const insertAfter = contractRef || orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0]; if (insertAfter && insertAfter.parentNode) { insertAfter.parentNode.insertBefore(projectRef, insertAfter.nextSibling); } } } /** * Enhances payment information from metadata * @param doc XML document * @param paymentInfo Payment information from metadata */ private enhancePaymentInformation(doc: Document, paymentInfo?: any): void { if (!paymentInfo) return; const root = doc.documentElement; let paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0]; // Create PaymentMeans if it doesn't exist if (!paymentMeans) { paymentMeans = doc.createElement('cac:PaymentMeans'); // Insert before TaxTotal const taxTotal = root.getElementsByTagName('cac:TaxTotal')[0]; if (taxTotal && taxTotal.parentNode) { taxTotal.parentNode.insertBefore(paymentMeans, taxTotal); } } // Add PaymentMeansCode if (paymentInfo.paymentMeansCode && !paymentMeans.getElementsByTagName('cbc:PaymentMeansCode')[0]) { const meansCode = doc.createElement('cbc:PaymentMeansCode'); meansCode.textContent = paymentInfo.paymentMeansCode; paymentMeans.appendChild(meansCode); } // Add PaymentID if (paymentInfo.paymentID && !paymentMeans.getElementsByTagName('cbc:PaymentID')[0]) { const paymentId = doc.createElement('cbc:PaymentID'); paymentId.textContent = paymentInfo.paymentID; paymentMeans.appendChild(paymentId); } // Add PaymentDueDate if (paymentInfo.paymentDueDate && !paymentMeans.getElementsByTagName('cbc:PaymentDueDate')[0]) { const dueDate = doc.createElement('cbc:PaymentDueDate'); dueDate.textContent = paymentInfo.paymentDueDate; paymentMeans.appendChild(dueDate); } // Add IBAN and BIC if (paymentInfo.iban || paymentInfo.bic) { let payeeAccount = paymentMeans.getElementsByTagName('cac:PayeeFinancialAccount')[0]; if (!payeeAccount) { payeeAccount = doc.createElement('cac:PayeeFinancialAccount'); paymentMeans.appendChild(payeeAccount); } // Add IBAN if (paymentInfo.iban && !payeeAccount.getElementsByTagName('cbc:ID')[0]) { const iban = doc.createElement('cbc:ID'); iban.textContent = paymentInfo.iban; payeeAccount.appendChild(iban); } // Add account name (must come after ID but before FinancialInstitutionBranch) if (paymentInfo.accountName && !payeeAccount.getElementsByTagName('cbc:Name')[0]) { const accountName = doc.createElement('cbc:Name'); accountName.textContent = paymentInfo.accountName; // Insert after ID but before FinancialInstitutionBranch const id = payeeAccount.getElementsByTagName('cbc:ID')[0]; const finInstBranch = payeeAccount.getElementsByTagName('cac:FinancialInstitutionBranch')[0]; if (finInstBranch) { payeeAccount.insertBefore(accountName, finInstBranch); } else if (id && id.nextSibling) { payeeAccount.insertBefore(accountName, id.nextSibling); } else { payeeAccount.appendChild(accountName); } } // Add BIC and bank name if (paymentInfo.bic || paymentInfo.bankName) { let finInstBranch = payeeAccount.getElementsByTagName('cac:FinancialInstitutionBranch')[0]; if (!finInstBranch) { finInstBranch = doc.createElement('cac:FinancialInstitutionBranch'); payeeAccount.appendChild(finInstBranch); } // Add BIC as branch ID if (paymentInfo.bic && !finInstBranch.getElementsByTagName('cbc:ID')[0]) { const bicElement = doc.createElement('cbc:ID'); bicElement.textContent = paymentInfo.bic; finInstBranch.appendChild(bicElement); } // Add bank name if (paymentInfo.bankName && !finInstBranch.getElementsByTagName('cbc:Name')[0]) { const bankNameElement = doc.createElement('cbc:Name'); bankNameElement.textContent = paymentInfo.bankName; finInstBranch.appendChild(bankNameElement); } } } // Add payment terms with discount if available if (paymentInfo.paymentTermsNote && paymentInfo.paymentTermsNote.includes('early payment')) { let paymentTerms = root.getElementsByTagName('cac:PaymentTerms')[0]; if (!paymentTerms) { paymentTerms = doc.createElement('cac:PaymentTerms'); // Insert before PaymentMeans const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0]; if (paymentMeans && paymentMeans.parentNode) { paymentMeans.parentNode.insertBefore(paymentTerms, paymentMeans); } } // Update or add note let note = paymentTerms.getElementsByTagName('cbc:Note')[0]; if (!note) { note = doc.createElement('cbc:Note'); paymentTerms.appendChild(note); } note.textContent = paymentInfo.paymentTermsNote; // Add discount percent if available if (paymentInfo.discountPercent && !paymentTerms.getElementsByTagName('cbc:SettlementDiscountPercent')[0]) { const discountElement = doc.createElement('cbc:SettlementDiscountPercent'); discountElement.textContent = paymentInfo.discountPercent; paymentTerms.appendChild(discountElement); } } } /** * Adds date information from metadata * @param doc XML document * @param dateInfo Date information from metadata */ private addDateInformation(doc: Document, dateInfo?: any): void { if (!dateInfo) return; const root = doc.documentElement; // Add InvoicePeriod if ((dateInfo.periodStart || dateInfo.periodEnd) && !root.getElementsByTagName('cac:InvoicePeriod')[0]) { const invoicePeriod = doc.createElement('cac:InvoicePeriod'); if (dateInfo.periodStart) { const startDate = doc.createElement('cbc:StartDate'); startDate.textContent = dateInfo.periodStart; invoicePeriod.appendChild(startDate); } if (dateInfo.periodEnd) { const endDate = doc.createElement('cbc:EndDate'); endDate.textContent = dateInfo.periodEnd; invoicePeriod.appendChild(endDate); } // Insert after business references or DocumentCurrencyCode const projectRef = root.getElementsByTagName('cac:ProjectReference')[0]; const contractRef = root.getElementsByTagName('cac:ContractDocumentReference')[0]; const orderRef = root.getElementsByTagName('cac:OrderReference')[0]; const insertAfter = projectRef || contractRef || orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0]; if (insertAfter && insertAfter.parentNode) { insertAfter.parentNode.insertBefore(invoicePeriod, insertAfter.nextSibling); } } // Add Delivery with ActualDeliveryDate if (dateInfo.deliveryDate && !root.getElementsByTagName('cac:Delivery')[0]) { const delivery = doc.createElement('cac:Delivery'); const deliveryDate = doc.createElement('cbc:ActualDeliveryDate'); deliveryDate.textContent = dateInfo.deliveryDate; delivery.appendChild(deliveryDate); // Insert before PaymentMeans const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0]; if (paymentMeans && paymentMeans.parentNode) { paymentMeans.parentNode.insertBefore(delivery, paymentMeans); } } } /** * Enhances party information with contact details from metadata * @param doc XML document * @param invoice Invoice data */ private enhancePartyInformation(doc: Document, invoice: TInvoice): void { // Enhance supplier party this.addContactToParty(doc, 'cac:AccountingSupplierParty', (invoice.from as any)?.metadata?.contactInformation); // Enhance customer party this.addContactToParty(doc, 'cac:AccountingCustomerParty', (invoice.to as any)?.metadata?.contactInformation); } /** * Adds contact information to a party * @param doc XML document * @param partySelector Party selector * @param contactInfo Contact information from metadata */ private addContactToParty(doc: Document, partySelector: string, contactInfo?: any): void { if (!contactInfo) return; const partyContainer = doc.getElementsByTagName(partySelector)[0]; if (!partyContainer) return; const party = partyContainer.getElementsByTagName('cac:Party')[0]; if (!party) return; // Check if Contact already exists let contact = party.getElementsByTagName('cac:Contact')[0]; if (!contact && (contactInfo.name || contactInfo.phone || contactInfo.email)) { contact = doc.createElement('cac:Contact'); // Insert after PartyName const partyName = party.getElementsByTagName('cac:PartyName')[0]; if (partyName && partyName.parentNode) { partyName.parentNode.insertBefore(contact, partyName.nextSibling); } else { party.appendChild(contact); } } if (contact) { // Add contact name if (contactInfo.name && !contact.getElementsByTagName('cbc:Name')[0]) { const name = doc.createElement('cbc:Name'); name.textContent = contactInfo.name; contact.appendChild(name); } // Add telephone if (contactInfo.phone && !contact.getElementsByTagName('cbc:Telephone')[0]) { const phone = doc.createElement('cbc:Telephone'); phone.textContent = contactInfo.phone; contact.appendChild(phone); } // Add email if (contactInfo.email && !contact.getElementsByTagName('cbc:ElectronicMail')[0]) { const email = doc.createElement('cbc:ElectronicMail'); email.textContent = contactInfo.email; contact.appendChild(email); } } } /** * Enhances line items with metadata * @param doc XML document * @param invoice Invoice data */ private enhanceLineItems(doc: Document, invoice: TInvoice): void { const invoiceLines = doc.getElementsByTagName('cac:InvoiceLine'); for (let i = 0; i < invoiceLines.length && i < invoice.items.length; i++) { const line = invoiceLines[i]; const item = invoice.items[i]; const itemMetadata = (item as any).metadata; if (!itemMetadata) continue; const itemElement = line.getElementsByTagName('cac:Item')[0]; if (!itemElement) continue; // Add item description if (itemMetadata.description && !itemElement.getElementsByTagName('cbc:Description')[0]) { const desc = doc.createElement('cbc:Description'); desc.textContent = itemMetadata.description; // Insert before Name const name = itemElement.getElementsByTagName('cbc:Name')[0]; if (name && name.parentNode) { name.parentNode.insertBefore(desc, name); } else { itemElement.appendChild(desc); } } // Add SellersItemIdentification if (itemMetadata.buyerItemID && !itemElement.getElementsByTagName('cac:SellersItemIdentification')[0]) { const sellerId = doc.createElement('cac:SellersItemIdentification'); const id = doc.createElement('cbc:ID'); id.textContent = item.articleNumber || itemMetadata.buyerItemID; sellerId.appendChild(id); itemElement.appendChild(sellerId); } // Add BuyersItemIdentification if (itemMetadata.buyerItemID && !itemElement.getElementsByTagName('cac:BuyersItemIdentification')[0]) { const buyerId = doc.createElement('cac:BuyersItemIdentification'); const id = doc.createElement('cbc:ID'); id.textContent = itemMetadata.buyerItemID; buyerId.appendChild(id); itemElement.appendChild(buyerId); } // Add StandardItemIdentification if (itemMetadata.standardItemID && !itemElement.getElementsByTagName('cac:StandardItemIdentification')[0]) { const standardId = doc.createElement('cac:StandardItemIdentification'); const id = doc.createElement('cbc:ID'); id.textContent = itemMetadata.standardItemID; standardId.appendChild(id); itemElement.appendChild(standardId); } // Add CommodityClassification if (itemMetadata.commodityClassification && !itemElement.getElementsByTagName('cac:CommodityClassification')[0]) { const classification = doc.createElement('cac:CommodityClassification'); const code = doc.createElement('cbc:ItemClassificationCode'); code.textContent = itemMetadata.commodityClassification; classification.appendChild(code); itemElement.appendChild(classification); } // Add additional item properties if (itemMetadata.additionalProperties) { for (const [propName, propValue] of Object.entries(itemMetadata.additionalProperties)) { const additionalProp = doc.createElement('cac:AdditionalItemProperty'); const nameElement = doc.createElement('cbc:Name'); nameElement.textContent = propName; additionalProp.appendChild(nameElement); const valueElement = doc.createElement('cbc:Value'); valueElement.textContent = propValue as string; additionalProp.appendChild(valueElement); itemElement.appendChild(additionalProp); } } } } }