feat(invoice): add e-invoice support with XRechnung/ZUGFeRD and advanced export features
This commit is contained in:
581
ts/skr.invoice.adapter.ts
Normal file
581
ts/skr.invoice.adapter.ts
Normal file
@@ -0,0 +1,581 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import type {
|
||||
IInvoice,
|
||||
IInvoiceLine,
|
||||
IInvoiceParty,
|
||||
IVATCategory,
|
||||
IValidationResult,
|
||||
TInvoiceFormat,
|
||||
TInvoiceDirection,
|
||||
TTaxScenario,
|
||||
IAllowanceCharge,
|
||||
IPaymentTerms
|
||||
} from './skr.invoice.entity.js';
|
||||
|
||||
/**
|
||||
* Adapter for @fin.cx/einvoice library
|
||||
* Handles parsing, validation, and format conversion of e-invoices
|
||||
*/
|
||||
export class InvoiceAdapter {
|
||||
private logger: plugins.smartlog.ConsoleLog;
|
||||
|
||||
constructor() {
|
||||
this.logger = new plugins.smartlog.ConsoleLog();
|
||||
}
|
||||
|
||||
private readonly MAX_XML_SIZE = 10 * 1024 * 1024; // 10MB max
|
||||
private readonly MAX_PDF_SIZE = 50 * 1024 * 1024; // 50MB max
|
||||
|
||||
/**
|
||||
* Parse an invoice from file or buffer
|
||||
*/
|
||||
public async parseInvoice(
|
||||
file: Buffer | string,
|
||||
direction: TInvoiceDirection
|
||||
): Promise<IInvoice> {
|
||||
try {
|
||||
// Validate input size
|
||||
if (Buffer.isBuffer(file)) {
|
||||
if (file.length > this.MAX_XML_SIZE) {
|
||||
throw new Error(`Invoice file too large: ${file.length} bytes (max ${this.MAX_XML_SIZE} bytes)`);
|
||||
}
|
||||
} else if (typeof file === 'string' && file.length > this.MAX_XML_SIZE) {
|
||||
throw new Error(`Invoice XML too large: ${file.length} characters (max ${this.MAX_XML_SIZE} characters)`);
|
||||
}
|
||||
|
||||
// Parse the invoice using @fin.cx/einvoice
|
||||
let einvoice;
|
||||
if (typeof file === 'string') {
|
||||
einvoice = await plugins.einvoice.EInvoice.fromXml(file);
|
||||
} else {
|
||||
// Convert buffer to string first
|
||||
const xmlString = file.toString('utf-8');
|
||||
einvoice = await plugins.einvoice.EInvoice.fromXml(xmlString);
|
||||
}
|
||||
|
||||
// Get detected format
|
||||
const format = this.mapEInvoiceFormat(einvoice.format || 'xrechnung');
|
||||
|
||||
// Validate the invoice (takes ~2.2ms)
|
||||
const validationResult = await this.validateInvoice(einvoice);
|
||||
|
||||
// Extract invoice data
|
||||
const invoiceData = einvoice.toObject();
|
||||
|
||||
// Map to internal invoice model
|
||||
const invoice = await this.mapToInternalModel(
|
||||
invoiceData,
|
||||
format,
|
||||
direction,
|
||||
validationResult
|
||||
);
|
||||
|
||||
// Store original XML content
|
||||
invoice.xmlContent = einvoice.getXml();
|
||||
|
||||
// Calculate content hash
|
||||
invoice.contentHash = await this.calculateContentHash(invoice.xmlContent);
|
||||
|
||||
// Classify tax scenario
|
||||
invoice.taxScenario = this.classifyTaxScenario(invoice);
|
||||
|
||||
return invoice;
|
||||
} catch (error) {
|
||||
this.logger.log('error', `Failed to parse invoice: ${error}`);
|
||||
throw new Error(`Invoice parsing failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an invoice using multi-level validation
|
||||
*/
|
||||
private async validateInvoice(einvoice: any): Promise<IValidationResult> {
|
||||
// Perform multi-level validation
|
||||
const validationResult = await einvoice.validate();
|
||||
|
||||
// Parse validation results into our structure
|
||||
const syntaxResult = {
|
||||
isValid: validationResult.syntax?.valid !== false,
|
||||
errors: validationResult.syntax?.errors || [],
|
||||
warnings: validationResult.syntax?.warnings || []
|
||||
};
|
||||
|
||||
const semanticResult = {
|
||||
isValid: validationResult.semantic?.valid !== false,
|
||||
errors: validationResult.semantic?.errors || [],
|
||||
warnings: validationResult.semantic?.warnings || []
|
||||
};
|
||||
|
||||
const businessResult = {
|
||||
isValid: validationResult.business?.valid !== false,
|
||||
errors: validationResult.business?.errors || [],
|
||||
warnings: validationResult.business?.warnings || []
|
||||
};
|
||||
|
||||
const countryResult = {
|
||||
isValid: validationResult.country?.valid !== false,
|
||||
errors: validationResult.country?.errors || [],
|
||||
warnings: validationResult.country?.warnings || []
|
||||
};
|
||||
|
||||
return {
|
||||
isValid: syntaxResult.isValid && semanticResult.isValid && businessResult.isValid,
|
||||
syntax: {
|
||||
valid: syntaxResult.isValid,
|
||||
errors: syntaxResult.errors || [],
|
||||
warnings: syntaxResult.warnings || []
|
||||
},
|
||||
semantic: {
|
||||
valid: semanticResult.isValid,
|
||||
errors: semanticResult.errors || [],
|
||||
warnings: semanticResult.warnings || []
|
||||
},
|
||||
businessRules: {
|
||||
valid: businessResult.isValid,
|
||||
errors: businessResult.errors || [],
|
||||
warnings: businessResult.warnings || []
|
||||
},
|
||||
countrySpecific: {
|
||||
valid: countryResult.isValid,
|
||||
errors: countryResult.errors || [],
|
||||
warnings: countryResult.warnings || []
|
||||
},
|
||||
validatedAt: new Date(),
|
||||
validatorVersion: '5.1.4'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map EN16931 Business Terms to internal invoice model
|
||||
*/
|
||||
private async mapToInternalModel(
|
||||
businessTerms: any,
|
||||
format: TInvoiceFormat,
|
||||
direction: TInvoiceDirection,
|
||||
validationResult: IValidationResult
|
||||
): Promise<IInvoice> {
|
||||
const invoice: IInvoice = {
|
||||
// Identity
|
||||
id: plugins.smartunique.shortId(),
|
||||
direction,
|
||||
format,
|
||||
|
||||
// EN16931 Business Terms
|
||||
invoiceNumber: businessTerms.BT1_InvoiceNumber,
|
||||
issueDate: new Date(businessTerms.BT2_IssueDate),
|
||||
invoiceTypeCode: businessTerms.BT3_InvoiceTypeCode || '380',
|
||||
currencyCode: businessTerms.BT5_CurrencyCode || 'EUR',
|
||||
taxCurrencyCode: businessTerms.BT6_TaxCurrencyCode,
|
||||
taxPointDate: businessTerms.BT7_TaxPointDate ? new Date(businessTerms.BT7_TaxPointDate) : undefined,
|
||||
paymentDueDate: businessTerms.BT9_PaymentDueDate ? new Date(businessTerms.BT9_PaymentDueDate) : undefined,
|
||||
buyerReference: businessTerms.BT10_BuyerReference,
|
||||
projectReference: businessTerms.BT11_ProjectReference,
|
||||
contractReference: businessTerms.BT12_ContractReference,
|
||||
orderReference: businessTerms.BT13_OrderReference,
|
||||
sellerOrderReference: businessTerms.BT14_SellerOrderReference,
|
||||
|
||||
// Parties
|
||||
supplier: this.mapParty(businessTerms.BG4_Seller),
|
||||
customer: this.mapParty(businessTerms.BG7_Buyer),
|
||||
payee: businessTerms.BG10_Payee ? this.mapParty(businessTerms.BG10_Payee) : undefined,
|
||||
|
||||
// Line items
|
||||
lines: this.mapInvoiceLines(businessTerms.BG25_InvoiceLines || []),
|
||||
|
||||
// Allowances and charges
|
||||
allowances: this.mapAllowancesCharges(businessTerms.BG20_DocumentAllowances || [], true),
|
||||
charges: this.mapAllowancesCharges(businessTerms.BG21_DocumentCharges || [], false),
|
||||
|
||||
// Amounts
|
||||
lineNetAmount: parseFloat(businessTerms.BT106_SumOfLineNetAmounts || 0),
|
||||
allowanceTotalAmount: parseFloat(businessTerms.BT107_AllowanceTotalAmount || 0),
|
||||
chargeTotalAmount: parseFloat(businessTerms.BT108_ChargeTotalAmount || 0),
|
||||
taxExclusiveAmount: parseFloat(businessTerms.BT109_TaxExclusiveAmount || 0),
|
||||
taxInclusiveAmount: parseFloat(businessTerms.BT112_TaxInclusiveAmount || 0),
|
||||
prepaidAmount: parseFloat(businessTerms.BT113_PrepaidAmount || 0),
|
||||
payableAmount: parseFloat(businessTerms.BT115_PayableAmount || 0),
|
||||
|
||||
// VAT breakdown
|
||||
vatBreakdown: this.mapVATBreakdown(businessTerms.BG23_VATBreakdown || []),
|
||||
totalVATAmount: parseFloat(businessTerms.BT110_TotalVATAmount || 0),
|
||||
|
||||
// Payment
|
||||
paymentTerms: this.mapPaymentTerms(businessTerms),
|
||||
paymentMeans: this.mapPaymentMeans(businessTerms.BG16_PaymentInstructions),
|
||||
|
||||
// Notes
|
||||
invoiceNote: businessTerms.BT22_InvoiceNote,
|
||||
|
||||
// Processing metadata
|
||||
status: 'validated',
|
||||
|
||||
// Storage (to be filled later)
|
||||
contentHash: '',
|
||||
|
||||
// Validation
|
||||
validationResult,
|
||||
|
||||
// Audit trail
|
||||
createdAt: new Date(),
|
||||
createdBy: 'system',
|
||||
|
||||
// Metadata
|
||||
metadata: {
|
||||
importedAt: new Date(),
|
||||
parserVersion: '5.1.4',
|
||||
originalFormat: format
|
||||
}
|
||||
};
|
||||
|
||||
return invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map party information
|
||||
*/
|
||||
private mapParty(partyData: any): IInvoiceParty {
|
||||
if (!partyData) {
|
||||
return {
|
||||
id: '',
|
||||
name: '',
|
||||
address: { countryCode: 'DE' }
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: partyData.BT29_SellerID || partyData.BT46_BuyerID || plugins.smartunique.shortId(),
|
||||
name: partyData.BT27_SellerName || partyData.BT44_BuyerName || '',
|
||||
address: {
|
||||
street: partyData.BT35_SellerStreet || partyData.BT50_BuyerStreet,
|
||||
city: partyData.BT37_SellerCity || partyData.BT52_BuyerCity,
|
||||
postalCode: partyData.BT38_SellerPostalCode || partyData.BT53_BuyerPostalCode,
|
||||
countryCode: partyData.BT40_SellerCountryCode || partyData.BT55_BuyerCountryCode || 'DE'
|
||||
},
|
||||
vatId: partyData.BT31_SellerVATID || partyData.BT48_BuyerVATID,
|
||||
taxId: partyData.BT32_SellerTaxID || partyData.BT47_BuyerTaxID,
|
||||
email: partyData.BT34_SellerEmail || partyData.BT49_BuyerEmail,
|
||||
phone: partyData.BT33_SellerPhone,
|
||||
bankAccount: this.mapBankAccount(partyData)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map bank account information
|
||||
*/
|
||||
private mapBankAccount(partyData: any): IInvoiceParty['bankAccount'] | undefined {
|
||||
if (!partyData?.BT84_PaymentAccountID) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
iban: partyData.BT84_PaymentAccountID,
|
||||
bic: partyData.BT86_PaymentServiceProviderID,
|
||||
accountHolder: partyData.BT85_PaymentAccountName
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map invoice lines
|
||||
*/
|
||||
private mapInvoiceLines(linesData: any[]): IInvoiceLine[] {
|
||||
return linesData.map((line, index) => ({
|
||||
lineNumber: index + 1,
|
||||
description: line.BT154_ItemDescription || '',
|
||||
quantity: parseFloat(line.BT129_Quantity || 1),
|
||||
unitPrice: parseFloat(line.BT146_NetPrice || 0),
|
||||
netAmount: parseFloat(line.BT131_LineNetAmount || 0),
|
||||
vatCategory: this.mapVATCategory(line.BT151_ItemVATCategory, line.BT152_ItemVATRate),
|
||||
vatAmount: parseFloat(line.lineVATAmount || 0),
|
||||
grossAmount: parseFloat(line.BT131_LineNetAmount || 0) + parseFloat(line.lineVATAmount || 0),
|
||||
productCode: line.BT155_ItemSellerID,
|
||||
allowances: this.mapLineAllowancesCharges(line.BG27_LineAllowances || [], true),
|
||||
charges: this.mapLineAllowancesCharges(line.BG28_LineCharges || [], false)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map VAT category
|
||||
*/
|
||||
private mapVATCategory(categoryCode: string, rate: string | number): IVATCategory {
|
||||
const vatRate = typeof rate === 'string' ? parseFloat(rate) : rate;
|
||||
|
||||
return {
|
||||
code: categoryCode || 'S',
|
||||
rate: vatRate || 0,
|
||||
exemptionReason: this.getExemptionReason(categoryCode)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exemption reason for VAT category
|
||||
*/
|
||||
private getExemptionReason(categoryCode: string): string | undefined {
|
||||
const exemptionReasons: Record<string, string> = {
|
||||
'E': 'Tax exempt',
|
||||
'Z': 'Zero rated',
|
||||
'AE': 'Reverse charge (§13b UStG)',
|
||||
'K': 'Intra-EU supply',
|
||||
'G': 'Export outside EU',
|
||||
'O': 'Outside scope of tax',
|
||||
'S': undefined // Standard rate, no exemption
|
||||
};
|
||||
|
||||
return exemptionReasons[categoryCode];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map VAT breakdown
|
||||
*/
|
||||
private mapVATBreakdown(vatBreakdown: any[]): IInvoice['vatBreakdown'] {
|
||||
return vatBreakdown.map(vat => ({
|
||||
vatCategory: this.mapVATCategory(vat.BT118_VATCategory, vat.BT119_VATRate),
|
||||
taxableAmount: parseFloat(vat.BT116_TaxableAmount || 0),
|
||||
taxAmount: parseFloat(vat.BT117_TaxAmount || 0)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map allowances and charges
|
||||
*/
|
||||
private mapAllowancesCharges(data: any[], isAllowance: boolean): IAllowanceCharge[] {
|
||||
return data.map(item => ({
|
||||
reason: item.BT97_AllowanceReason || item.BT104_ChargeReason || '',
|
||||
amount: parseFloat(item.BT92_AllowanceAmount || item.BT99_ChargeAmount || 0),
|
||||
percentage: item.BT94_AllowancePercentage || item.BT101_ChargePercentage,
|
||||
vatCategory: item.BT95_AllowanceVATCategory || item.BT102_ChargeVATCategory
|
||||
? this.mapVATCategory(
|
||||
item.BT95_AllowanceVATCategory || item.BT102_ChargeVATCategory,
|
||||
item.BT96_AllowanceVATRate || item.BT103_ChargeVATRate
|
||||
)
|
||||
: undefined,
|
||||
vatAmount: parseFloat(item.allowanceVATAmount || item.chargeVATAmount || 0)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map line-level allowances and charges
|
||||
*/
|
||||
private mapLineAllowancesCharges(data: any[], isAllowance: boolean): IAllowanceCharge[] {
|
||||
return data.map(item => ({
|
||||
reason: item.BT140_LineAllowanceReason || item.BT145_LineChargeReason || '',
|
||||
amount: parseFloat(item.BT136_LineAllowanceAmount || item.BT141_LineChargeAmount || 0),
|
||||
percentage: item.BT138_LineAllowancePercentage || item.BT143_LineChargePercentage
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map payment terms
|
||||
*/
|
||||
private mapPaymentTerms(businessTerms: any): IPaymentTerms | undefined {
|
||||
if (!businessTerms.BT9_PaymentDueDate && !businessTerms.BT20_PaymentTerms) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const paymentTerms: IPaymentTerms = {
|
||||
dueDate: businessTerms.BT9_PaymentDueDate
|
||||
? new Date(businessTerms.BT9_PaymentDueDate)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // Default 30 days
|
||||
paymentTermsNote: businessTerms.BT20_PaymentTerms
|
||||
};
|
||||
|
||||
// Parse skonto from payment terms note if present
|
||||
if (businessTerms.BT20_PaymentTerms) {
|
||||
paymentTerms.skonto = this.parseSkontoTerms(businessTerms.BT20_PaymentTerms);
|
||||
}
|
||||
|
||||
return paymentTerms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse skonto terms from payment terms text
|
||||
*/
|
||||
private parseSkontoTerms(paymentTermsText: string): IPaymentTerms['skonto'] {
|
||||
const skontoTerms: IPaymentTerms['skonto'] = [];
|
||||
|
||||
// Common German skonto patterns:
|
||||
// "2% Skonto bei Zahlung innerhalb von 10 Tagen"
|
||||
// "3% bei Zahlung bis 8 Tage, 2% bis 14 Tage"
|
||||
const skontoPattern = /(\d+(?:\.\d+)?)\s*%.*?(\d+)\s*(?:Tag|Day)/gi;
|
||||
let match;
|
||||
|
||||
while ((match = skontoPattern.exec(paymentTermsText)) !== null) {
|
||||
skontoTerms.push({
|
||||
percentage: parseFloat(match[1]),
|
||||
days: parseInt(match[2]),
|
||||
baseAmount: 0 // To be calculated based on invoice amount
|
||||
});
|
||||
}
|
||||
|
||||
return skontoTerms.length > 0 ? skontoTerms : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map payment means
|
||||
*/
|
||||
private mapPaymentMeans(paymentInstructions: any): IInvoice['paymentMeans'] | undefined {
|
||||
if (!paymentInstructions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
code: paymentInstructions.BT81_PaymentMeansCode || '30', // 30 = Bank transfer
|
||||
account: paymentInstructions.BT84_PaymentAccountID
|
||||
? {
|
||||
iban: paymentInstructions.BT84_PaymentAccountID,
|
||||
bic: paymentInstructions.BT86_PaymentServiceProviderID,
|
||||
accountHolder: paymentInstructions.BT85_PaymentAccountName
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify tax scenario based on invoice data
|
||||
*/
|
||||
private classifyTaxScenario(invoice: IInvoice): TTaxScenario {
|
||||
const supplierCountry = invoice.supplier.address.countryCode;
|
||||
const customerCountry = invoice.customer.address.countryCode;
|
||||
const hasVAT = invoice.totalVATAmount > 0;
|
||||
const vatCategories = invoice.vatBreakdown.map(vb => vb.vatCategory.code);
|
||||
|
||||
// Reverse charge
|
||||
if (vatCategories.includes('AE')) {
|
||||
return 'reverse_charge';
|
||||
}
|
||||
|
||||
// Small business exemption
|
||||
if (vatCategories.includes('E') && invoice.invoiceNote?.includes('§19')) {
|
||||
return 'small_business';
|
||||
}
|
||||
|
||||
// Export outside EU
|
||||
if (vatCategories.includes('G') || (!this.isEUCountry(customerCountry) && supplierCountry === 'DE')) {
|
||||
return 'export';
|
||||
}
|
||||
|
||||
// Intra-EU transactions
|
||||
if (supplierCountry !== customerCountry && this.isEUCountry(supplierCountry) && this.isEUCountry(customerCountry)) {
|
||||
if (invoice.direction === 'outbound') {
|
||||
return 'intra_eu_supply';
|
||||
} else {
|
||||
return 'intra_eu_acquisition';
|
||||
}
|
||||
}
|
||||
|
||||
// Domestic exempt
|
||||
if (!hasVAT && supplierCountry === 'DE' && customerCountry === 'DE') {
|
||||
return 'domestic_exempt';
|
||||
}
|
||||
|
||||
// Default: Domestic taxed
|
||||
return 'domestic_taxed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if country is in EU
|
||||
*/
|
||||
private isEUCountry(countryCode: string): boolean {
|
||||
const euCountries = [
|
||||
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
|
||||
'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
|
||||
'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'
|
||||
];
|
||||
return euCountries.includes(countryCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map e-invoice format from library format
|
||||
*/
|
||||
private mapEInvoiceFormat(format: string): TInvoiceFormat {
|
||||
const formatMap: Record<string, TInvoiceFormat> = {
|
||||
'xrechnung': 'xrechnung',
|
||||
'zugferd': 'zugferd',
|
||||
'factur-x': 'facturx',
|
||||
'facturx': 'facturx',
|
||||
'peppol': 'peppol',
|
||||
'ubl': 'ubl'
|
||||
};
|
||||
|
||||
return formatMap[format.toLowerCase()] || 'xrechnung';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate content hash for the invoice
|
||||
*/
|
||||
private async calculateContentHash(xmlContent: string): Promise<string> {
|
||||
const hash = await plugins.smarthash.sha256FromString(xmlContent);
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert invoice to different format
|
||||
*/
|
||||
public async convertFormat(
|
||||
invoice: IInvoice,
|
||||
targetFormat: TInvoiceFormat
|
||||
): Promise<string> {
|
||||
try {
|
||||
// Load from existing XML
|
||||
const einvoice = await plugins.einvoice.EInvoice.fromXml(invoice.xmlContent!);
|
||||
|
||||
// Convert to target format (takes ~0.6ms)
|
||||
const convertedXml = await einvoice.exportXml(targetFormat as any);
|
||||
|
||||
return convertedXml;
|
||||
} catch (error) {
|
||||
this.logger.log('error', `Failed to convert invoice format: ${error}`);
|
||||
throw new Error(`Format conversion failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate invoice from internal data
|
||||
*/
|
||||
public async generateInvoice(
|
||||
invoiceData: Partial<IInvoice>,
|
||||
format: TInvoiceFormat
|
||||
): Promise<{ xml: string; pdf?: Buffer }> {
|
||||
try {
|
||||
// Create a new invoice instance
|
||||
const einvoice = new plugins.einvoice.EInvoice();
|
||||
|
||||
// Set invoice data
|
||||
const businessTerms = this.mapToBusinessTerms(invoiceData);
|
||||
Object.assign(einvoice, businessTerms);
|
||||
|
||||
// Generate XML in requested format
|
||||
const xml = await einvoice.exportXml(format as any);
|
||||
|
||||
// Generate PDF if ZUGFeRD or Factur-X
|
||||
let pdf: Buffer | undefined;
|
||||
if (format === 'zugferd' || format === 'facturx') {
|
||||
// Access the pdf property if it exists
|
||||
if (einvoice.pdf && einvoice.pdf.buffer) {
|
||||
pdf = Buffer.from(einvoice.pdf.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
return { xml, pdf };
|
||||
} catch (error) {
|
||||
this.logger.log('error', `Failed to generate invoice: ${error}`);
|
||||
throw new Error(`Invoice generation failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map internal invoice to EN16931 Business Terms
|
||||
*/
|
||||
private mapToBusinessTerms(invoice: Partial<IInvoice>): any {
|
||||
return {
|
||||
BT1_InvoiceNumber: invoice.invoiceNumber,
|
||||
BT2_IssueDate: invoice.issueDate?.toISOString(),
|
||||
BT3_InvoiceTypeCode: invoice.invoiceTypeCode || '380',
|
||||
BT5_CurrencyCode: invoice.currencyCode || 'EUR',
|
||||
BT7_TaxPointDate: invoice.taxPointDate?.toISOString(),
|
||||
BT9_PaymentDueDate: invoice.paymentDueDate?.toISOString(),
|
||||
|
||||
// Map other Business Terms...
|
||||
// This would be a comprehensive mapping in production
|
||||
};
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user