466 lines
17 KiB
TypeScript
466 lines
17 KiB
TypeScript
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 'xmldom';
|
|
|
|
/**
|
|
* 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<string> {
|
|
// 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<string> {
|
|
// 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}`;
|
|
}
|
|
}
|