import { UBLBaseDecoder } from '../ubl.decoder.js'; import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js'; import { business, finance } from '../../../plugins.js'; 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, accountingDocType: 'creditnote' as const } as unknown 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, accountingDocType: 'debitnote' as const } as unknown 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.TAccountingDocItem[] = []; 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 description = this.getText('./cac:Item/cbc:Description', line) || ''; const articleNumber = this.getText('./cac:Item/cac:SellersItemIdentification/cbc:ID', line) || ''; const buyerItemID = this.getText('./cac:Item/cac:BuyersItemIdentification/cbc:ID', line) || ''; const standardItemID = this.getText('./cac:Item/cac:StandardItemIdentification/cbc:ID', line) || ''; const commodityClassification = this.getText('./cac:Item/cac:CommodityClassification/cbc:ItemClassificationCode', 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; } // Create item with extended metadata const item: finance.TAccountingDocItem & { metadata?: any } = { position, name, articleNumber, unitType, unitQuantity, unitNetPrice, vatPercentage }; // Extract order line reference const orderLineReference = this.getText('./cac:OrderLineReference/cac:OrderReference/cbc:ID', line) || ''; const orderLineReferenceId = this.getText('./cac:OrderLineReference/cbc:LineID', line) || ''; // Extract additional item properties const additionalProps: Record = {}; const propNodes = this.select('./cac:Item/cac:AdditionalItemProperty', line); if (propNodes && Array.isArray(propNodes)) { for (const propNode of propNodes) { const propName = this.getText('./cbc:Name', propNode); const propValue = this.getText('./cbc:Value', propNode); if (propName && propValue) { additionalProps[propName] = propValue; } } } // Store additional item data in metadata if (description || buyerItemID || standardItemID || commodityClassification || orderLineReference || Object.keys(additionalProps).length > 0) { item.metadata = { description, buyerItemID, standardItemID, commodityClassification, orderLineReference, orderLineReferenceId, additionalProperties: additionalProps }; } items.push(item); } } // Extract business references const buyerReference = this.getText('//cbc:BuyerReference', this.doc); const orderReference = this.getText('//cac:OrderReference/cbc:ID', this.doc); const contractReference = this.getText('//cac:ContractDocumentReference/cbc:ID', this.doc); const projectReference = this.getText('//cac:ProjectReference/cbc:ID', this.doc); // Extract payment information const paymentMeansCode = this.getText('//cac:PaymentMeans/cbc:PaymentMeansCode', this.doc); const paymentID = this.getText('//cac:PaymentMeans/cbc:PaymentID', this.doc); const paymentDueDate = this.getText('//cac:PaymentMeans/cbc:PaymentDueDate', this.doc); const iban = this.getText('//cac:PaymentMeans/cac:PayeeFinancialAccount/cbc:ID', this.doc); const bic = this.getText('//cac:PaymentMeans/cac:PayeeFinancialAccount/cac:FinancialInstitutionBranch/cbc:ID', this.doc); const bankName = this.getText('//cac:PaymentMeans/cac:PayeeFinancialAccount/cac:FinancialInstitutionBranch/cbc:Name', this.doc); const accountName = this.getText('//cac:PaymentMeans/cac:PayeeFinancialAccount/cbc:Name', this.doc); // Extract payment terms with discount const paymentTermsNote = this.getText('//cac:PaymentTerms/cbc:Note', this.doc); const discountPercent = this.getText('//cac:PaymentTerms/cbc:SettlementDiscountPercent', this.doc); // Extract period information const periodStart = this.getText('//cac:InvoicePeriod/cbc:StartDate', this.doc); const periodEnd = this.getText('//cac:InvoicePeriod/cbc:EndDate', this.doc); const deliveryDate = this.getText('//cac:Delivery/cbc:ActualDeliveryDate', this.doc); // 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 with metadata for business references const invoiceData: any = { type: 'accounting-doc' as const, accountingDocType: 'invoice' as const, id: invoiceId, accountingDocId: invoiceId, date: issueDate, accountingDocStatus: 'issued' as const, versionInfo: { type: 'final', version: '1.0.0' }, language: 'en', incidenceId: invoiceId, from: seller, to: buyer, subject: notes.length > 0 ? notes[0] : `Invoice ${invoiceId}`, items: items, dueInDays: dueInDays, reverseCharge: false, currency: currencyCode as finance.TCurrency, notes: notes, objectActions: [], metadata: { format: 'xrechnung' as any, version: '1.0.0', extensions: { businessReferences: { buyerReference, orderReference, contractReference, projectReference }, paymentInformation: { paymentMeansCode, paymentID, paymentDueDate, iban, bic, bankName, accountName, paymentTermsNote, discountPercent }, dateInformation: { periodStart, periodEnd, deliveryDate } } } }; return invoiceData; } catch (error) { console.error('Error extracting common data:', error); // Return default data return { type: 'accounting-doc' as const, accountingDocType: 'invoice' as const, id: `INV-${Date.now()}`, accountingDocId: `INV-${Date.now()}`, date: Date.now(), accountingDocStatus: 'issued' as const, 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 = ''; let contactPhone = ''; let contactEmail = ''; let contactName = ''; let gln = ''; const additionalIdentifiers: any[] = []; // 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 GLN from EndpointID const endpointId = this.getText('./cbc:EndpointID[@schemeID="0088"]', party); if (endpointId) { gln = endpointId; } // Extract additional party identifications const partyIdNodes = this.select('./cac:PartyIdentification', party); if (partyIdNodes && Array.isArray(partyIdNodes)) { for (const idNode of partyIdNodes) { const idValue = this.getText('./cbc:ID', idNode); const idElement = (idNode as Element).getElementsByTagName('cbc:ID')[0]; const schemeId = idElement?.getAttribute('schemeID'); if (idValue) { additionalIdentifiers.push({ value: idValue, scheme: schemeId || '' }); } } } // 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; } // Extract contact information const contactNodes = this.select('./cac:Contact', party); if (contactNodes && Array.isArray(contactNodes) && contactNodes.length > 0) { const contact = contactNodes[0]; contactPhone = this.getText('./cbc:Telephone', contact) || ''; contactEmail = this.getText('./cbc:ElectronicMail', contact) || ''; contactName = this.getText('./cbc:Name', contact) || ''; } } // Create contact with additional metadata for contact information const contact: business.TContact & { metadata?: any } = { 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 } }; // Store contact information and additional identifiers in metadata if available const metadata: any = {}; if (contactPhone || contactEmail || contactName) { metadata.contactInformation = { phone: contactPhone, email: contactEmail, name: contactName }; } if (gln) { (contact as any).gln = gln; } if (additionalIdentifiers.length > 0) { (contact as any).additionalIdentifiers = additionalIdentifiers; } if (Object.keys(metadata).length > 0) { contact.metadata = metadata; } return contact; } 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: '' } }; } }