import { UBLBaseEncoder } from '../ubl.encoder.js'; import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js'; import { UBLDocumentType } from '../ubl.types.js'; import { DOMParser, XMLSerializer } from '../../../plugins.js'; /** * UBL Encoder implementation * Provides encoding functionality for UBL 2.1 invoice and credit note documents */ export class UBLEncoder extends UBLBaseEncoder { /** * Encodes a credit note into UBL XML * @param creditNote Credit note to encode * @returns UBL XML string */ protected async encodeCreditNote(creditNote: TCreditNote): Promise { // Create XML document from template const xmlString = this.createXmlRoot(UBLDocumentType.CREDIT_NOTE); const doc = new DOMParser().parseFromString(xmlString, 'application/xml'); // Add common document elements this.addCommonElements(doc, creditNote as unknown as TInvoice, UBLDocumentType.CREDIT_NOTE); // Add credit note specific data this.addCreditNoteSpecificData(doc, creditNote); // Serialize to string return new XMLSerializer().serializeToString(doc); } /** * Encodes a debit note (invoice) into UBL XML * @param debitNote Debit note to encode * @returns UBL XML string */ protected async encodeDebitNote(debitNote: TDebitNote): Promise { // Create XML document from template const xmlString = this.createXmlRoot(UBLDocumentType.INVOICE); const doc = new DOMParser().parseFromString(xmlString, 'application/xml'); // Add common document elements this.addCommonElements(doc, debitNote as unknown as TInvoice, UBLDocumentType.INVOICE); // Add invoice specific data this.addInvoiceSpecificData(doc, debitNote); // Serialize to string return new XMLSerializer().serializeToString(doc); } /** * Adds common document elements to both invoice and credit note * @param doc XML document * @param invoice Invoice or credit note data * @param documentType Document type (Invoice or CreditNote) */ private addCommonElements(doc: Document, invoice: TInvoice, documentType: UBLDocumentType): void { const root = doc.documentElement; // UBL Version ID (2.1 is standard for EN16931) this.appendElement(doc, root, 'cbc:UBLVersionID', '2.1'); // Customization ID - using generic UBL this.appendElement(doc, root, 'cbc:CustomizationID', 'urn:cen.eu:en16931:2017'); // Profile ID - standard billing this.appendElement(doc, root, 'cbc:ProfileID', 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0'); // ID this.appendElement(doc, root, 'cbc:ID', invoice.id); // Issue Date this.appendElement(doc, root, 'cbc:IssueDate', this.formatDate(invoice.date)); // 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 const typeCode = documentType === UBLDocumentType.INVOICE ? '380' : '381'; this.appendElement(doc, root, 'cbc:InvoiceTypeCode', typeCode); // Notes if (invoice.notes && invoice.notes.length > 0) { for (const note of invoice.notes) { this.appendElement(doc, root, 'cbc:Note', note); } } // Document Currency Code this.appendElement(doc, root, 'cbc:DocumentCurrencyCode', invoice.currency); // Add accounting supplier party (seller) this.addParty(doc, root, 'cac:AccountingSupplierParty', invoice.from); // Add accounting customer party (buyer) this.addParty(doc, root, 'cac:AccountingCustomerParty', invoice.to); // Add payment terms this.addPaymentTerms(doc, root, invoice); // Add tax summary this.addTaxTotal(doc, root, invoice); // Add monetary totals this.addLegalMonetaryTotal(doc, root, invoice); // Add line items this.addInvoiceLines(doc, root, invoice); // Preserve metadata if available this.preserveMetadata(doc, root, invoice); } /** * Adds credit note specific data to the document * @param doc XML document * @param creditNote Credit note data */ private addCreditNoteSpecificData(doc: Document, creditNote: TCreditNote): void { // For now, there's no specific data to add for credit notes // If needed, additional credit note specific fields would be added here } /** * Adds invoice specific data to the document * @param doc XML document * @param invoice Invoice data */ private addInvoiceSpecificData(doc: Document, invoice: TDebitNote): void { // For now, there's no specific data to add for invoices that's not already covered // If needed, additional invoice specific fields would be added here } /** * Adds party information (supplier or customer) * @param doc XML document * @param parentElement Parent element * @param elementName Element name (AccountingSupplierParty or AccountingCustomerParty) * @param party Party data */ private addParty(doc: Document, parentElement: Element, elementName: string, party: any): void { const partyElement = doc.createElement(elementName); parentElement.appendChild(partyElement); const partyNode = doc.createElement('cac:Party'); partyElement.appendChild(partyNode); // Party name const partyNameNode = doc.createElement('cac:PartyName'); partyNode.appendChild(partyNameNode); this.appendElement(doc, partyNameNode, 'cbc:Name', party.name); // Postal address const postalAddressNode = doc.createElement('cac:PostalAddress'); partyNode.appendChild(postalAddressNode); if (party.address.streetName) { this.appendElement(doc, postalAddressNode, 'cbc:StreetName', party.address.streetName); } if (party.address.houseNumber && party.address.houseNumber !== '0') { this.appendElement(doc, postalAddressNode, 'cbc:BuildingNumber', party.address.houseNumber); } if (party.address.city) { this.appendElement(doc, postalAddressNode, 'cbc:CityName', party.address.city); } if (party.address.postalCode) { this.appendElement(doc, postalAddressNode, 'cbc:PostalZone', party.address.postalCode); } // Country if (party.address.country || party.address.countryCode) { const countryNode = doc.createElement('cac:Country'); postalAddressNode.appendChild(countryNode); const countryCode = party.address.countryCode || this.getCountryCode(party.address.country); this.appendElement(doc, countryNode, 'cbc:IdentificationCode', countryCode); if (party.address.country) { this.appendElement(doc, countryNode, 'cbc:Name', party.address.country); } } // Party tax scheme (VAT ID) if (party.registrationDetails && party.registrationDetails.vatId) { const partyTaxSchemeNode = doc.createElement('cac:PartyTaxScheme'); partyNode.appendChild(partyTaxSchemeNode); this.appendElement(doc, partyTaxSchemeNode, 'cbc:CompanyID', party.registrationDetails.vatId); const taxSchemeNode = doc.createElement('cac:TaxScheme'); partyTaxSchemeNode.appendChild(taxSchemeNode); this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'VAT'); } // Party legal entity (registration information) if (party.registrationDetails) { const partyLegalEntityNode = doc.createElement('cac:PartyLegalEntity'); partyNode.appendChild(partyLegalEntityNode); const registrationName = party.registrationDetails.registrationName || party.name; this.appendElement(doc, partyLegalEntityNode, 'cbc:RegistrationName', registrationName); if (party.registrationDetails.registrationId) { this.appendElement(doc, partyLegalEntityNode, 'cbc:CompanyID', party.registrationDetails.registrationId); } } // Contact information if (party.contactDetails) { const contactNode = doc.createElement('cac:Contact'); partyNode.appendChild(contactNode); if (party.contactDetails.name) { this.appendElement(doc, contactNode, 'cbc:Name', party.contactDetails.name); } if (party.contactDetails.telephone) { this.appendElement(doc, contactNode, 'cbc:Telephone', party.contactDetails.telephone); } if (party.contactDetails.email) { this.appendElement(doc, contactNode, 'cbc:ElectronicMail', party.contactDetails.email); } } } /** * Adds payment terms information * @param doc XML document * @param parentElement Parent element * @param invoice Invoice data */ private addPaymentTerms(doc: Document, parentElement: Element, invoice: TInvoice): void { const paymentTermsNode = doc.createElement('cac:PaymentTerms'); parentElement.appendChild(paymentTermsNode); // Payment terms note this.appendElement(doc, paymentTermsNode, 'cbc:Note', `Due in ${invoice.dueInDays} days`); // Add payment means if available if (invoice.paymentOptions) { this.addPaymentMeans(doc, parentElement, invoice); } } /** * Adds payment means information * @param doc XML document * @param parentElement Parent element * @param invoice Invoice data */ private addPaymentMeans(doc: Document, parentElement: Element, invoice: TInvoice): void { const paymentMeansNode = doc.createElement('cac:PaymentMeans'); parentElement.appendChild(paymentMeansNode); // Payment means code - default to credit transfer this.appendElement(doc, paymentMeansNode, 'cbc:PaymentMeansCode', '30'); // 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 if (invoice.paymentOptions.description) { this.appendElement(doc, paymentMeansNode, 'cbc:InstructionNote', invoice.paymentOptions.description); } // Add payment ID information if available - use invoice ID as payment reference this.appendElement(doc, paymentMeansNode, 'cbc:PaymentID', invoice.id); // Add bank account information if available if (invoice.paymentOptions.sepaConnection && invoice.paymentOptions.sepaConnection.iban) { const payeeFinancialAccountNode = doc.createElement('cac:PayeeFinancialAccount'); paymentMeansNode.appendChild(payeeFinancialAccountNode); this.appendElement(doc, payeeFinancialAccountNode, 'cbc:ID', invoice.paymentOptions.sepaConnection.iban); // Add financial institution information if BIC is available if (invoice.paymentOptions.sepaConnection.bic) { const financialInstitutionNode = doc.createElement('cac:FinancialInstitutionBranch'); payeeFinancialAccountNode.appendChild(financialInstitutionNode); this.appendElement(doc, financialInstitutionNode, 'cbc:ID', invoice.paymentOptions.sepaConnection.bic); } } } /** * Adds tax total information * @param doc XML document * @param parentElement Parent element * @param invoice Invoice data */ private addTaxTotal(doc: Document, parentElement: Element, invoice: TInvoice): void { const taxTotalNode = doc.createElement('cac:TaxTotal'); parentElement.appendChild(taxTotalNode); // Calculate total tax amount let totalTaxAmount = 0; const taxCategories = new Map(); // Map of VAT rate to net amount // Calculate from items if (invoice.items) { for (const item of invoice.items) { const itemNetAmount = item.unitNetPrice * item.unitQuantity; const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100); const vatRate = item.vatPercentage; totalTaxAmount += itemTaxAmount; // Aggregate by VAT rate const currentAmount = taxCategories.get(vatRate) || 0; taxCategories.set(vatRate, currentAmount + itemNetAmount); } } // Add total tax amount const taxAmountElement = doc.createElement('cbc:TaxAmount'); taxAmountElement.setAttribute('currencyID', invoice.currency); taxAmountElement.textContent = totalTaxAmount.toFixed(2); taxTotalNode.appendChild(taxAmountElement); // Add tax subtotals for (const [rate, baseAmount] of taxCategories.entries()) { const taxSubtotalNode = doc.createElement('cac:TaxSubtotal'); taxTotalNode.appendChild(taxSubtotalNode); // Taxable amount const taxableAmountElement = doc.createElement('cbc:TaxableAmount'); taxableAmountElement.setAttribute('currencyID', invoice.currency); taxableAmountElement.textContent = baseAmount.toFixed(2); taxSubtotalNode.appendChild(taxableAmountElement); // Tax amount const taxAmount = baseAmount * (rate / 100); const subtotalTaxAmountElement = doc.createElement('cbc:TaxAmount'); subtotalTaxAmountElement.setAttribute('currencyID', invoice.currency); subtotalTaxAmountElement.textContent = taxAmount.toFixed(2); taxSubtotalNode.appendChild(subtotalTaxAmountElement); // Tax category const taxCategoryNode = doc.createElement('cac:TaxCategory'); taxSubtotalNode.appendChild(taxCategoryNode); // Determine tax category ID based on reverse charge const categoryId = invoice.reverseCharge ? 'AE' : 'S'; this.appendElement(doc, taxCategoryNode, 'cbc:ID', categoryId); // Add percent with 2 decimal places this.appendElement(doc, taxCategoryNode, 'cbc:Percent', rate.toFixed(2)); // Add tax exemption reason if reverse charge if (invoice.reverseCharge) { this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReasonCode', 'VATEX-EU-IC'); this.appendElement(doc, taxCategoryNode, 'cbc:TaxExemptionReason', 'Reverse charge'); } // Add tax scheme const taxSchemeNode = doc.createElement('cac:TaxScheme'); taxCategoryNode.appendChild(taxSchemeNode); this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'VAT'); } } /** * Adds legal monetary total information * @param doc XML document * @param parentElement Parent element * @param invoice Invoice data */ private addLegalMonetaryTotal(doc: Document, parentElement: Element, invoice: TInvoice): void { const legalMonetaryTotalNode = doc.createElement('cac:LegalMonetaryTotal'); parentElement.appendChild(legalMonetaryTotalNode); // Calculate totals let totalNetAmount = 0; let totalTaxAmount = 0; // Calculate from items if (invoice.items) { for (const item of invoice.items) { const itemNetAmount = item.unitNetPrice * item.unitQuantity; const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100); totalNetAmount += itemNetAmount; totalTaxAmount += itemTaxAmount; } } const totalGrossAmount = totalNetAmount + totalTaxAmount; // Line extension amount (sum of line net amounts) const lineExtensionAmountElement = doc.createElement('cbc:LineExtensionAmount'); lineExtensionAmountElement.setAttribute('currencyID', invoice.currency); lineExtensionAmountElement.textContent = totalNetAmount.toFixed(2); legalMonetaryTotalNode.appendChild(lineExtensionAmountElement); // Tax exclusive amount const taxExclusiveAmountElement = doc.createElement('cbc:TaxExclusiveAmount'); taxExclusiveAmountElement.setAttribute('currencyID', invoice.currency); taxExclusiveAmountElement.textContent = totalNetAmount.toFixed(2); legalMonetaryTotalNode.appendChild(taxExclusiveAmountElement); // Tax inclusive amount const taxInclusiveAmountElement = doc.createElement('cbc:TaxInclusiveAmount'); taxInclusiveAmountElement.setAttribute('currencyID', invoice.currency); taxInclusiveAmountElement.textContent = totalGrossAmount.toFixed(2); legalMonetaryTotalNode.appendChild(taxInclusiveAmountElement); // Payable amount const payableAmountElement = doc.createElement('cbc:PayableAmount'); payableAmountElement.setAttribute('currencyID', invoice.currency); payableAmountElement.textContent = totalGrossAmount.toFixed(2); legalMonetaryTotalNode.appendChild(payableAmountElement); } /** * Adds invoice lines * @param doc XML document * @param parentElement Parent element * @param invoice Invoice data */ private addInvoiceLines(doc: Document, parentElement: Element, invoice: TInvoice): void { if (!invoice.items) return; for (const item of invoice.items) { const invoiceLineNode = doc.createElement('cac:InvoiceLine'); parentElement.appendChild(invoiceLineNode); // ID this.appendElement(doc, invoiceLineNode, 'cbc:ID', item.position.toString()); // Invoiced quantity const quantityElement = doc.createElement('cbc:InvoicedQuantity'); quantityElement.setAttribute('unitCode', item.unitType); quantityElement.textContent = item.unitQuantity.toString(); invoiceLineNode.appendChild(quantityElement); // Line extension amount (line net amount) const itemNetAmount = item.unitNetPrice * item.unitQuantity; const lineExtensionAmountElement = doc.createElement('cbc:LineExtensionAmount'); lineExtensionAmountElement.setAttribute('currencyID', invoice.currency); lineExtensionAmountElement.textContent = itemNetAmount.toFixed(2); invoiceLineNode.appendChild(lineExtensionAmountElement); // Item information const itemNode = doc.createElement('cac:Item'); invoiceLineNode.appendChild(itemNode); // Description this.appendElement(doc, itemNode, 'cbc:Description', item.name); this.appendElement(doc, itemNode, 'cbc:Name', item.name); // Seller's item identification if (item.articleNumber) { const sellersItemIdentificationNode = doc.createElement('cac:SellersItemIdentification'); itemNode.appendChild(sellersItemIdentificationNode); this.appendElement(doc, sellersItemIdentificationNode, 'cbc:ID', item.articleNumber); } // Item tax information const classifiedTaxCategoryNode = doc.createElement('cac:ClassifiedTaxCategory'); itemNode.appendChild(classifiedTaxCategoryNode); // Determine tax category ID based on reverse charge const categoryId = invoice.reverseCharge ? 'AE' : 'S'; this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:ID', categoryId); // Tax percent with 2 decimal places this.appendElement(doc, classifiedTaxCategoryNode, 'cbc:Percent', item.vatPercentage.toFixed(2)); // Tax scheme const taxSchemeNode = doc.createElement('cac:TaxScheme'); classifiedTaxCategoryNode.appendChild(taxSchemeNode); this.appendElement(doc, taxSchemeNode, 'cbc:ID', 'VAT'); // Price information const priceNode = doc.createElement('cac:Price'); invoiceLineNode.appendChild(priceNode); // Price amount const priceAmountElement = doc.createElement('cbc:PriceAmount'); priceAmountElement.setAttribute('currencyID', invoice.currency); priceAmountElement.textContent = item.unitNetPrice.toFixed(2); priceNode.appendChild(priceAmountElement); } } /** * Helper method to append a simple element with text content * @param doc XML document * @param parentElement Parent element * @param elementName Element name * @param textContent Text content */ private appendElement(doc: Document, parentElement: Element, elementName: string, textContent: string): void { const element = doc.createElement(elementName); element.textContent = textContent; parentElement.appendChild(element); } /** * Helper method to get country code from country name * Simple implementation that assumes the country name is already a code * @param countryName Country name * @returns Country code (2-letter ISO code) */ private getCountryCode(countryName: string): string { // In a real implementation, this would map country names to ISO codes // For now, just return the first 2 characters or "XX" as fallback if (!countryName) return 'XX'; return countryName.length >= 2 ? countryName.substring(0, 2).toUpperCase() : 'XX'; } /** * Preserves metadata from invoice to enhance UBL XML output * @param doc XML document * @param root Root element * @param invoice Invoice data */ private preserveMetadata(doc: Document, root: Element, invoice: TInvoice): void { // Extract metadata if available const metadata = (invoice as any).metadata?.extensions; if (!metadata) return; // Preserve business references this.addBusinessReferencesToUBL(doc, root, metadata.businessReferences); // Preserve payment information this.enhancePaymentInformationUBL(doc, root, metadata.paymentInformation); // Preserve date information this.addDateInformationUBL(doc, root, metadata.dateInformation); // Enhance party information with contact details this.enhancePartyInformationUBL(doc, invoice); // Enhance line items with metadata this.enhanceLineItemsUBL(doc, invoice); } /** * Adds business references from metadata to UBL document * @param doc XML document * @param root Root element * @param businessReferences Business references from metadata */ private addBusinessReferencesToUBL(doc: Document, root: Element, businessReferences?: any): void { if (!businessReferences) return; // 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 in UBL document * @param doc XML document * @param root Root element * @param paymentInfo Payment information from metadata */ private enhancePaymentInformationUBL(doc: Document, root: Element, paymentInfo?: any): void { if (!paymentInfo) return; 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 to UBL document * @param doc XML document * @param root Root element * @param dateInfo Date information from metadata */ private addDateInformationUBL(doc: Document, root: Element, dateInfo?: any): void { if (!dateInfo) return; // 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 enhancePartyInformationUBL(doc: Document, invoice: TInvoice): void { // Enhance supplier party this.enhancePartyUBL(doc, 'cac:AccountingSupplierParty', invoice.from); // Enhance customer party this.enhancePartyUBL(doc, 'cac:AccountingCustomerParty', invoice.to); } /** * Enhances a party with GLN, additional identifiers, and contact info * @param doc XML document * @param partySelector Party selector * @param partyData Party data */ private enhancePartyUBL(doc: Document, partySelector: string, partyData: any): void { if (!partyData) return; const partyContainer = doc.getElementsByTagName(partySelector)[0]; if (!partyContainer) return; const party = partyContainer.getElementsByTagName('cac:Party')[0]; if (!party) return; // Add GLN if available if (partyData.gln && !party.getElementsByTagName('cbc:EndpointID')[0]) { const endpointNode = doc.createElement('cbc:EndpointID'); endpointNode.setAttribute('schemeID', '0088'); // GLN scheme ID endpointNode.textContent = partyData.gln; // Insert as first child if (party.firstChild) { party.insertBefore(endpointNode, party.firstChild); } else { party.appendChild(endpointNode); } } // Add additional identifiers if (partyData.additionalIdentifiers && Array.isArray(partyData.additionalIdentifiers)) { for (const identifier of partyData.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 = party.getElementsByTagName('cbc:EndpointID')[0]; if (endpoint && endpoint.nextSibling) { party.insertBefore(partyId, endpoint.nextSibling); } else if (party.firstChild) { party.insertBefore(partyId, party.firstChild); } else { party.appendChild(partyId); } } } // Add contact information from metadata if not already present const contactInfo = partyData.metadata?.contactInformation; if (contactInfo) { this.addContactToPartyUBL(doc, partySelector, contactInfo); } } /** * Adds contact information to a party in UBL document * @param doc XML document * @param partySelector Party selector * @param contactInfo Contact information from metadata */ private addContactToPartyUBL(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 in UBL document * @param doc XML document * @param invoice Invoice data */ private enhanceLineItemsUBL(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; // Add OrderLineReference if available if (itemMetadata.orderLineReference && !line.getElementsByTagName('cac:OrderLineReference')[0]) { const orderLineRef = doc.createElement('cac:OrderLineReference'); const lineId = doc.createElement('cbc:LineID'); lineId.textContent = itemMetadata.orderLineReferenceId || '1'; orderLineRef.appendChild(lineId); if (itemMetadata.orderLineReference) { const orderRef = doc.createElement('cac:OrderReference'); const orderId = doc.createElement('cbc:ID'); orderId.textContent = itemMetadata.orderLineReference; orderRef.appendChild(orderId); orderLineRef.appendChild(orderRef); } // Insert after ID const invoiceLineId = line.getElementsByTagName('cbc:ID')[0]; if (invoiceLineId && invoiceLineId.nextSibling) { line.insertBefore(orderLineRef, invoiceLineId.nextSibling); } else { // Insert before InvoicedQuantity const quantity = line.getElementsByTagName('cbc:InvoicedQuantity')[0]; if (quantity) { line.insertBefore(orderLineRef, quantity); } } } 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 (item.articleNumber && !itemElement.getElementsByTagName('cac:SellersItemIdentification')[0]) { const sellerId = doc.createElement('cac:SellersItemIdentification'); const id = doc.createElement('cbc:ID'); id.textContent = item.articleNumber; 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); } } } } }