import { UBLBaseDecoder } from '../ubl.decoder.js'; import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js'; import { business, finance } from '@tsclass/tsclass'; import { UBLDocumentType } from '../ubl.types.js'; /** * Decoder for XRechnung (UBL) format * Implements decoding of XRechnung invoices to TInvoice */ export class XRechnungDecoder extends UBLBaseDecoder { /** * Decodes a UBL credit note * @returns Promise resolving to a TCreditNote object */ protected async decodeCreditNote(): Promise { // Extract common data const commonData = await this.extractCommonData(); // Return the invoice data as a credit note return { ...commonData, invoiceType: 'creditnote' } as TCreditNote; } /** * Decodes a UBL debit note (invoice) * @returns Promise resolving to a TDebitNote object */ protected async decodeDebitNote(): Promise { // Extract common data const commonData = await this.extractCommonData(); // Return the invoice data as a debit note return { ...commonData, invoiceType: 'debitnote' } as TDebitNote; } /** * Extracts common invoice data from XRechnung XML * @returns Common invoice data */ private async extractCommonData(): Promise> { try { // Default values const invoiceId = this.getText('//cbc:ID', this.doc) || `INV-${Date.now()}`; const issueDateText = this.getText('//cbc:IssueDate', this.doc); const issueDate = issueDateText ? new Date(issueDateText).getTime() : Date.now(); const currencyCode = this.getText('//cbc:DocumentCurrencyCode', this.doc) || 'EUR'; // Extract payment terms let dueInDays = 30; // Default const dueDateText = this.getText('//cac:PaymentTerms/cbc:PaymentDueDate', this.doc); if (dueDateText) { const dueDateObj = new Date(dueDateText); const issueDateObj = new Date(issueDate); const diffTime = Math.abs(dueDateObj.getTime() - issueDateObj.getTime()); dueInDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } // Extract items const items: finance.TInvoiceItem[] = []; const invoiceLines = this.select('//cac:InvoiceLine', this.doc); if (invoiceLines && Array.isArray(invoiceLines)) { for (let i = 0; i < invoiceLines.length; i++) { const line = invoiceLines[i]; const position = i + 1; const name = this.getText('./cac:Item/cbc:Name', line) || `Item ${position}`; const articleNumber = this.getText('./cac:Item/cac:SellersItemIdentification/cbc:ID', line) || ''; const unitType = this.getText('./cbc:InvoicedQuantity/@unitCode', line) || 'EA'; let unitQuantity = 1; const quantityText = this.getText('./cbc:InvoicedQuantity', line); if (quantityText) { unitQuantity = parseFloat(quantityText) || 1; } let unitNetPrice = 0; const priceText = this.getText('./cac:Price/cbc:PriceAmount', line); if (priceText) { unitNetPrice = parseFloat(priceText) || 0; } let vatPercentage = 0; const percentText = this.getText('./cac:Item/cac:ClassifiedTaxCategory/cbc:Percent', line); if (percentText) { vatPercentage = parseFloat(percentText) || 0; } items.push({ position, name, articleNumber, unitType, unitQuantity, unitNetPrice, vatPercentage }); } } // Extract notes const notes: string[] = []; const noteNodes = this.select('//cbc:Note', this.doc); if (noteNodes && Array.isArray(noteNodes)) { for (let i = 0; i < noteNodes.length; i++) { const noteText = noteNodes[i].textContent || ''; if (noteText) { notes.push(noteText); } } } // Extract seller and buyer information const seller = this.extractParty('//cac:AccountingSupplierParty/cac:Party'); const buyer = this.extractParty('//cac:AccountingCustomerParty/cac:Party'); // Create the common invoice data return { type: 'invoice', id: invoiceId, invoiceId: invoiceId, date: issueDate, status: 'invoice', versionInfo: { type: 'final', version: '1.0.0' }, language: 'en', incidenceId: invoiceId, from: seller, to: buyer, subject: `Invoice ${invoiceId}`, items: items, dueInDays: dueInDays, reverseCharge: false, currency: currencyCode as finance.TCurrency, notes: notes, objectActions: [] }; } catch (error) { console.error('Error extracting common data:', error); // Return default data return { type: 'invoice', id: `INV-${Date.now()}`, invoiceId: `INV-${Date.now()}`, date: Date.now(), status: 'invoice', versionInfo: { type: 'final', version: '1.0.0' }, language: 'en', incidenceId: `INV-${Date.now()}`, from: this.createEmptyContact(), to: this.createEmptyContact(), subject: 'Invoice', items: [], dueInDays: 30, reverseCharge: false, currency: 'EUR', notes: [], objectActions: [] }; } } /** * Extracts party information from XML * @param partyPath XPath to the party element * @returns TContact object */ private extractParty(partyPath: string): business.TContact { try { // Default values let name = ''; let streetName = ''; let houseNumber = '0'; let city = ''; let postalCode = ''; let country = ''; let countryCode = ''; let vatId = ''; let registrationId = ''; let registrationName = ''; // Try to extract party information const partyNodes = this.select(partyPath, this.doc); if (partyNodes && Array.isArray(partyNodes) && partyNodes.length > 0) { const party = partyNodes[0]; // Extract name name = this.getText('./cac:PartyName/cbc:Name', party) || ''; // Extract address const addressNodes = this.select('./cac:PostalAddress', party); if (addressNodes && Array.isArray(addressNodes) && addressNodes.length > 0) { const address = addressNodes[0]; streetName = this.getText('./cbc:StreetName', address) || ''; houseNumber = this.getText('./cbc:BuildingNumber', address) || '0'; city = this.getText('./cbc:CityName', address) || ''; postalCode = this.getText('./cbc:PostalZone', address) || ''; const countryNodes = this.select('./cac:Country', address); if (countryNodes && Array.isArray(countryNodes) && countryNodes.length > 0) { const countryNode = countryNodes[0]; country = this.getText('./cbc:Name', countryNode) || ''; countryCode = this.getText('./cbc:IdentificationCode', countryNode) || ''; } } // Extract tax information const taxSchemeNodes = this.select('./cac:PartyTaxScheme', party); if (taxSchemeNodes && Array.isArray(taxSchemeNodes) && taxSchemeNodes.length > 0) { vatId = this.getText('./cbc:CompanyID', taxSchemeNodes[0]) || ''; } // Extract registration information const legalEntityNodes = this.select('./cac:PartyLegalEntity', party); if (legalEntityNodes && Array.isArray(legalEntityNodes) && legalEntityNodes.length > 0) { registrationId = this.getText('./cbc:CompanyID', legalEntityNodes[0]) || ''; registrationName = this.getText('./cbc:RegistrationName', legalEntityNodes[0]) || name; } } return { type: 'company', name: name, description: '', address: { streetName: streetName, houseNumber: houseNumber, city: city, postalCode: postalCode, country: country, countryCode: countryCode }, status: 'active', foundedDate: { year: 2000, month: 1, day: 1 }, registrationDetails: { vatId: vatId, registrationId: registrationId, registrationName: registrationName } }; } catch (error) { console.error('Error extracting party information:', error); return this.createEmptyContact(); } } /** * Creates an empty TContact object * @returns Empty TContact object */ private createEmptyContact(): business.TContact { return { type: 'company', name: '', description: '', address: { streetName: '', houseNumber: '0', city: '', country: '', postalCode: '' }, status: 'active', foundedDate: { year: 2000, month: 1, day: 1 }, registrationDetails: { vatId: '', registrationId: '', registrationName: '' } }; } }