import { CIIBaseDecoder } from '../cii.decoder.js'; import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js'; import { ZUGFERD_PROFILE_IDS } from './zugferd.types.js'; import { business, finance, general } from '@tsclass/tsclass'; /** * Decoder for ZUGFeRD invoice format */ export class ZUGFeRDDecoder extends CIIBaseDecoder { /** * Decodes a ZUGFeRD credit note * @returns Promise resolving to a TCreditNote object */ protected async decodeCreditNote(): Promise { // Get common invoice data const commonData = await this.extractCommonData(); // Create a credit note with the common data return { ...commonData, invoiceType: 'creditnote' } as TCreditNote; } /** * Decodes a ZUGFeRD debit note (invoice) * @returns Promise resolving to a TDebitNote object */ protected async decodeDebitNote(): Promise { // Get common invoice data const commonData = await this.extractCommonData(); // Create a debit note with the common data return { ...commonData, invoiceType: 'debitnote' } as TDebitNote; } /** * Extracts common invoice data from ZUGFeRD XML * @returns Common invoice data */ private async extractCommonData(): Promise> { // Extract invoice ID const invoiceId = this.getText('//rsm:ExchangedDocument/ram:ID'); // Extract issue date const issueDateStr = this.getText('//ram:IssueDateTime/udt:DateTimeString'); const issueDate = issueDateStr ? new Date(issueDateStr).getTime() : Date.now(); // Extract seller information const seller = this.extractParty('//ram:SellerTradeParty'); // Extract buyer information const buyer = this.extractParty('//ram:BuyerTradeParty'); // Extract items const items = this.extractItems(); // Extract due date const dueDateStr = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString'); const dueDate = dueDateStr ? new Date(dueDateStr).getTime() : Date.now(); const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24)); // Extract currency const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR'; // Extract total amount const totalAmount = this.getNumber('//ram:GrandTotalAmount'); // Extract notes const notes = this.extractNotes(); // Check for reverse charge const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]'); // Create the common invoice data return { type: 'invoice', id: 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: reverseCharge, currency: currencyCode as finance.TCurrency, notes: notes, deliveryDate: issueDate, objectActions: [], invoiceType: 'debitnote' // Default to debit note, will be overridden in decode methods }; } /** * Extracts party information from ZUGFeRD XML * @param partyXPath XPath to the party node * @returns Party information as TContact */ private extractParty(partyXPath: string): business.TContact { // Extract name const name = this.getText(`${partyXPath}/ram:Name`); // Extract address const street = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:LineOne`); const city = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CityName`); const zip = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:PostcodeCode`); const country = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CountryID`); // Create address object const address = { street: street, city: city, zip: zip, country: country }; // Extract VAT ID const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || ''; // Extract registration ID const registrationId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) || ''; // Create contact object return { type: 'company', name: name, description: '', address: address, status: 'active', foundedDate: this.createDefaultDate(), registrationDetails: { vatId: vatId, registrationId: registrationId, registrationName: '' } } as business.TContact; } /** * Extracts invoice items from ZUGFeRD XML * @returns Array of invoice items */ private extractItems(): finance.TInvoiceItem[] { const items: finance.TInvoiceItem[] = []; // Get all item nodes const itemNodes = this.select('//ram:IncludedSupplyChainTradeLineItem', this.doc); // Process each item if (Array.isArray(itemNodes)) { for (let i = 0; i < itemNodes.length; i++) { const itemNode = itemNodes[i]; // Extract item data const name = this.getText('ram:SpecifiedTradeProduct/ram:Name', itemNode); const articleNumber = this.getText('ram:SpecifiedTradeProduct/ram:SellerAssignedID', itemNode); const unitQuantity = this.getNumber('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', itemNode); const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode) || 'EA'; const unitNetPrice = this.getNumber('ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount', itemNode); const vatPercentage = this.getNumber('ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', itemNode); // Create item object items.push({ position: i + 1, name: name, articleNumber: articleNumber, unitType: unitType, unitQuantity: unitQuantity, unitNetPrice: unitNetPrice, vatPercentage: vatPercentage }); } } return items; } /** * Extracts notes from ZUGFeRD XML * @returns Array of notes */ private extractNotes(): string[] { const notes: string[] = []; // Get all note nodes const noteNodes = this.select('//ram:IncludedNote', this.doc); // Process each note if (Array.isArray(noteNodes)) { for (let i = 0; i < noteNodes.length; i++) { const noteNode = noteNodes[i]; const noteText = this.getText('ram:Content', noteNode); if (noteText) { notes.push(noteText); } } } return notes; } /** * Creates a default date for empty date fields * @returns Default date as timestamp */ private createDefaultDate(): number { return new Date('2000-01-01').getTime(); } }