xinvoice/ts/formats/cii/zugferd/zugferd.v1.decoder.ts

249 lines
7.5 KiB
TypeScript

import { CIIBaseDecoder } from '../cii.decoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
import { ZUGFERD_V1_NAMESPACES } from '../cii.types.js';
import { business, finance } from '@tsclass/tsclass';
/**
* Decoder for ZUGFeRD v1 invoice format
*/
export class ZUGFeRDV1Decoder extends CIIBaseDecoder {
/**
* Constructor
* @param xml XML string to decode
*/
constructor(xml: string) {
super(xml);
// Override namespaces for ZUGFeRD v1
this.namespaces = {
rsm: ZUGFERD_V1_NAMESPACES.RSM,
ram: ZUGFERD_V1_NAMESPACES.RAM,
udt: ZUGFERD_V1_NAMESPACES.UDT
};
}
/**
* Decodes a ZUGFeRD v1 credit note
* @returns Promise resolving to a TCreditNote object
*/
protected async decodeCreditNote(): Promise<TCreditNote> {
// 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 v1 debit note (invoice)
* @returns Promise resolving to a TDebitNote object
*/
protected async decodeDebitNote(): Promise<TDebitNote> {
// 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 v1 XML
* @returns Common invoice data
*/
private async extractCommonData(): Promise<Partial<TInvoice>> {
// Extract invoice ID
const invoiceId = this.getText('//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 (not used in this implementation but could be useful)
// 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 v1 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 streetName = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:LineOne`);
const city = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CityName`);
const postalCode = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:PostcodeCode`);
const country = this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CountryID`);
// Try to extract house number from street if possible
let houseNumber = '';
const streetParts = streetName.match(/^(.*?)\s+(\d+.*)$/);
if (streetParts) {
// If we can split into street name and house number
houseNumber = streetParts[2];
}
// Create address object
const address = {
streetName: streetName,
houseNumber: houseNumber,
city: city,
postalCode: postalCode,
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 v1 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 v1 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 object compatible with TContact
*/
private createDefaultDate(): any {
// Create a date object that will be compatible with TContact
return {
year: 2000,
month: 1,
day: 1
};
}
}