import { CIIBaseEncoder } from '../cii.encoder.js'; import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js'; import { FACTURX_PROFILE_IDS } from './facturx.types.js'; import { DOMParser, XMLSerializer } from '../../../plugins.js'; /** * Encoder for Factur-X invoice format */ export class FacturXEncoder extends CIIBaseEncoder { /** * Encodes a TCreditNote object into Factur-X XML * @param creditNote TCreditNote object to encode * @returns Factur-X XML string */ protected async encodeCreditNote(creditNote: TCreditNote): Promise { // Create base XML const xmlDoc = this.createBaseXml(); // Set document type code to credit note (381) this.setDocumentTypeCode(xmlDoc, '381'); // Add common invoice data this.addCommonInvoiceData(xmlDoc, creditNote); // Serialize to string return new XMLSerializer().serializeToString(xmlDoc); } /** * Encodes a TDebitNote object into Factur-X XML * @param debitNote TDebitNote object to encode * @returns Factur-X XML string */ protected async encodeDebitNote(debitNote: TDebitNote): Promise { // Create base XML const xmlDoc = this.createBaseXml(); // Set document type code to invoice (380) this.setDocumentTypeCode(xmlDoc, '380'); // Add common invoice data this.addCommonInvoiceData(xmlDoc, debitNote); // Serialize to string return new XMLSerializer().serializeToString(xmlDoc); } /** * Creates a base Factur-X XML document * @returns XML document with basic structure */ private createBaseXml(): Document { // Create XML document from template const xmlString = this.createXmlRoot(); const doc = new DOMParser().parseFromString(xmlString, 'application/xml'); // Add Factur-X profile this.addProfile(doc); return doc; } /** * Adds Factur-X profile information to the XML document * @param doc XML document */ private addProfile(doc: Document): void { // Get root element const root = doc.documentElement; // Create context element if it doesn't exist let contextElement = root.getElementsByTagName('rsm:ExchangedDocumentContext')[0]; if (!contextElement) { contextElement = doc.createElement('rsm:ExchangedDocumentContext'); root.appendChild(contextElement); } // Create guideline parameter element const guidelineElement = doc.createElement('ram:GuidelineSpecifiedDocumentContextParameter'); contextElement.appendChild(guidelineElement); // Add ID element with profile const idElement = doc.createElement('ram:ID'); // Set profile based on the selected profile let profileId = FACTURX_PROFILE_IDS.EN16931; if (this.profile === 'BASIC') { profileId = FACTURX_PROFILE_IDS.BASIC; } else if (this.profile === 'MINIMUM') { profileId = FACTURX_PROFILE_IDS.MINIMUM; } idElement.textContent = profileId; guidelineElement.appendChild(idElement); } /** * Sets the document type code in the XML document * @param doc XML document * @param typeCode Document type code (380 for invoice, 381 for credit note) */ private setDocumentTypeCode(doc: Document, typeCode: string): void { // Get root element const root = doc.documentElement; // Create document element if it doesn't exist let documentElement = root.getElementsByTagName('rsm:ExchangedDocument')[0]; if (!documentElement) { documentElement = doc.createElement('rsm:ExchangedDocument'); root.appendChild(documentElement); } // Add type code element const typeCodeElement = doc.createElement('ram:TypeCode'); typeCodeElement.textContent = typeCode; documentElement.appendChild(typeCodeElement); } /** * Adds common invoice data to the XML document * @param doc XML document * @param invoice Invoice data */ private addCommonInvoiceData(doc: Document, invoice: TInvoice): void { // Get root element const root = doc.documentElement; // Get document element or create it let documentElement = root.getElementsByTagName('rsm:ExchangedDocument')[0]; if (!documentElement) { documentElement = doc.createElement('rsm:ExchangedDocument'); root.appendChild(documentElement); } // Add ID element const idElement = doc.createElement('ram:ID'); idElement.textContent = invoice.id; documentElement.appendChild(idElement); // Add issue date element const issueDateElement = doc.createElement('ram:IssueDateTime'); const dateStringElement = doc.createElement('udt:DateTimeString'); dateStringElement.setAttribute('format', '102'); // YYYYMMDD format dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date); issueDateElement.appendChild(dateStringElement); documentElement.appendChild(issueDateElement); // Create transaction element if it doesn't exist let transactionElement = root.getElementsByTagName('rsm:SupplyChainTradeTransaction')[0]; if (!transactionElement) { transactionElement = doc.createElement('rsm:SupplyChainTradeTransaction'); root.appendChild(transactionElement); } // Add agreement section with seller and buyer this.addAgreementSection(doc, transactionElement, invoice); // Add delivery section this.addDeliverySection(doc, transactionElement, invoice); // Add settlement section with payment terms and totals this.addSettlementSection(doc, transactionElement, invoice); // Add line items this.addLineItems(doc, transactionElement, invoice); } /** * Adds agreement section with seller and buyer information * @param doc XML document * @param transactionElement Transaction element * @param invoice Invoice data */ private addAgreementSection(doc: Document, transactionElement: Element, invoice: TInvoice): void { // Create agreement element const agreementElement = doc.createElement('ram:ApplicableHeaderTradeAgreement'); transactionElement.appendChild(agreementElement); // Add seller const sellerElement = doc.createElement('ram:SellerTradeParty'); this.addPartyInfo(doc, sellerElement, invoice.from); agreementElement.appendChild(sellerElement); // Add buyer const buyerElement = doc.createElement('ram:BuyerTradeParty'); this.addPartyInfo(doc, buyerElement, invoice.to); agreementElement.appendChild(buyerElement); } /** * Adds party information to an element * @param doc XML document * @param partyElement Party element * @param party Party data */ private addPartyInfo(doc: Document, partyElement: Element, party: any): void { // Add name const nameElement = doc.createElement('ram:Name'); nameElement.textContent = party.name; partyElement.appendChild(nameElement); // Add postal address const addressElement = doc.createElement('ram:PostalTradeAddress'); // Add address line 1 (street) const line1Element = doc.createElement('ram:LineOne'); line1Element.textContent = party.address.streetName; addressElement.appendChild(line1Element); // Add address line 2 (house number) const line2Element = doc.createElement('ram:LineTwo'); line2Element.textContent = party.address.houseNumber; addressElement.appendChild(line2Element); // Add postal code const postalCodeElement = doc.createElement('ram:PostcodeCode'); postalCodeElement.textContent = party.address.postalCode; addressElement.appendChild(postalCodeElement); // Add city const cityElement = doc.createElement('ram:CityName'); cityElement.textContent = party.address.city; addressElement.appendChild(cityElement); // Add country const countryElement = doc.createElement('ram:CountryID'); countryElement.textContent = party.address.countryCode || party.address.country; addressElement.appendChild(countryElement); partyElement.appendChild(addressElement); // Add VAT ID if available if (party.registrationDetails && party.registrationDetails.vatId) { const taxRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration'); const taxIdElement = doc.createElement('ram:ID'); taxIdElement.setAttribute('schemeID', 'VA'); taxIdElement.textContent = party.registrationDetails.vatId; taxRegistrationElement.appendChild(taxIdElement); partyElement.appendChild(taxRegistrationElement); } // Add registration ID if available if (party.registrationDetails && party.registrationDetails.registrationId) { const regRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration'); const regIdElement = doc.createElement('ram:ID'); regIdElement.setAttribute('schemeID', 'FC'); regIdElement.textContent = party.registrationDetails.registrationId; regRegistrationElement.appendChild(regIdElement); partyElement.appendChild(regRegistrationElement); } } /** * Adds delivery section with delivery information * @param doc XML document * @param transactionElement Transaction element * @param invoice Invoice data */ private addDeliverySection(doc: Document, transactionElement: Element, invoice: TInvoice): void { // Create delivery element const deliveryElement = doc.createElement('ram:ApplicableHeaderTradeDelivery'); transactionElement.appendChild(deliveryElement); // Add delivery date if available if (invoice.deliveryDate) { const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent'); const occurrenceDateElement = doc.createElement('ram:OccurrenceDateTime'); const dateStringElement = doc.createElement('udt:DateTimeString'); dateStringElement.setAttribute('format', '102'); // YYYYMMDD format dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate); occurrenceDateElement.appendChild(dateStringElement); deliveryDateElement.appendChild(occurrenceDateElement); deliveryElement.appendChild(deliveryDateElement); } } /** * Adds settlement section with payment terms and totals * @param doc XML document * @param transactionElement Transaction element * @param invoice Invoice data */ private addSettlementSection(doc: Document, transactionElement: Element, invoice: TInvoice): void { // Create settlement element const settlementElement = doc.createElement('ram:ApplicableHeaderTradeSettlement'); transactionElement.appendChild(settlementElement); // Add currency const currencyElement = doc.createElement('ram:InvoiceCurrencyCode'); currencyElement.textContent = invoice.currency; settlementElement.appendChild(currencyElement); // Add payment terms const paymentTermsElement = doc.createElement('ram:SpecifiedTradePaymentTerms'); // Add due date const dueDateElement = doc.createElement('ram:DueDateDateTime'); const dateStringElement = doc.createElement('udt:DateTimeString'); dateStringElement.setAttribute('format', '102'); // YYYYMMDD format // Calculate due date const dueDate = new Date(invoice.date); dueDate.setDate(dueDate.getDate() + invoice.dueInDays); dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime()); dueDateElement.appendChild(dateStringElement); paymentTermsElement.appendChild(dueDateElement); settlementElement.appendChild(paymentTermsElement); // Add totals const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation'); // 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; // Add line total amount const lineTotalElement = doc.createElement('ram:LineTotalAmount'); lineTotalElement.textContent = totalNetAmount.toFixed(2); monetarySummationElement.appendChild(lineTotalElement); // Add tax total amount const taxTotalElement = doc.createElement('ram:TaxTotalAmount'); taxTotalElement.textContent = totalTaxAmount.toFixed(2); taxTotalElement.setAttribute('currencyID', invoice.currency); monetarySummationElement.appendChild(taxTotalElement); // Add grand total amount const grandTotalElement = doc.createElement('ram:GrandTotalAmount'); grandTotalElement.textContent = totalGrossAmount.toFixed(2); monetarySummationElement.appendChild(grandTotalElement); // Add due payable amount const duePayableElement = doc.createElement('ram:DuePayableAmount'); duePayableElement.textContent = totalGrossAmount.toFixed(2); monetarySummationElement.appendChild(duePayableElement); settlementElement.appendChild(monetarySummationElement); } /** * Adds line items to the XML document * @param doc XML document * @param transactionElement Transaction element * @param invoice Invoice data */ private addLineItems(doc: Document, transactionElement: Element, invoice: TInvoice): void { // Add each line item if (invoice.items) { for (const item of invoice.items) { // Create line item element const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem'); // Add line ID const lineIdElement = doc.createElement('ram:AssociatedDocumentLineDocument'); const lineIdValueElement = doc.createElement('ram:LineID'); lineIdValueElement.textContent = item.position.toString(); lineIdElement.appendChild(lineIdValueElement); lineItemElement.appendChild(lineIdElement); // Add product information const productElement = doc.createElement('ram:SpecifiedTradeProduct'); // Add name const nameElement = doc.createElement('ram:Name'); nameElement.textContent = item.name; productElement.appendChild(nameElement); // Add article number if available if (item.articleNumber) { const articleNumberElement = doc.createElement('ram:SellerAssignedID'); articleNumberElement.textContent = item.articleNumber; productElement.appendChild(articleNumberElement); } lineItemElement.appendChild(productElement); // Add agreement information (price) const agreementElement = doc.createElement('ram:SpecifiedLineTradeAgreement'); const priceElement = doc.createElement('ram:NetPriceProductTradePrice'); const chargeAmountElement = doc.createElement('ram:ChargeAmount'); chargeAmountElement.textContent = item.unitNetPrice.toFixed(2); priceElement.appendChild(chargeAmountElement); agreementElement.appendChild(priceElement); lineItemElement.appendChild(agreementElement); // Add delivery information (quantity) const deliveryElement = doc.createElement('ram:SpecifiedLineTradeDelivery'); const quantityElement = doc.createElement('ram:BilledQuantity'); quantityElement.textContent = item.unitQuantity.toString(); quantityElement.setAttribute('unitCode', item.unitType); deliveryElement.appendChild(quantityElement); lineItemElement.appendChild(deliveryElement); // Add settlement information (tax) const settlementElement = doc.createElement('ram:SpecifiedLineTradeSettlement'); // Add tax information const taxElement = doc.createElement('ram:ApplicableTradeTax'); // Add tax type code const taxTypeCodeElement = doc.createElement('ram:TypeCode'); taxTypeCodeElement.textContent = 'VAT'; taxElement.appendChild(taxTypeCodeElement); // Add tax category code const taxCategoryCodeElement = doc.createElement('ram:CategoryCode'); taxCategoryCodeElement.textContent = 'S'; taxElement.appendChild(taxCategoryCodeElement); // Add tax rate const taxRateElement = doc.createElement('ram:RateApplicablePercent'); taxRateElement.textContent = item.vatPercentage.toString(); taxElement.appendChild(taxRateElement); settlementElement.appendChild(taxElement); // Add monetary summation const monetarySummationElement = doc.createElement('ram:SpecifiedLineTradeSettlementMonetarySummation'); // Calculate item total const itemNetAmount = item.unitNetPrice * item.unitQuantity; // Add line total amount const lineTotalElement = doc.createElement('ram:LineTotalAmount'); lineTotalElement.textContent = itemNetAmount.toFixed(2); monetarySummationElement.appendChild(lineTotalElement); settlementElement.appendChild(monetarySummationElement); lineItemElement.appendChild(settlementElement); // Add line item to transaction transactionElement.appendChild(lineItemElement); } } } /** * Formats a date as YYYYMMDD * @param timestamp Timestamp to format * @returns Formatted date string */ private formatDateYYYYMMDD(timestamp: number): string { const date = new Date(timestamp); const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); return `${year}${month}${day}`; } }