const generatePDF = async (reportData: any) => { // Import pdfmake with virtual file system for server-side use const pdfMake = await import('pdfmake/build/pdfmake.js') const pdfFonts = await import('pdfmake/build/vfs_fonts.js') // @ts-ignore - Set up virtual file system const pdfMakeInstance = pdfMake.default || pdfMake // Extract vfs from different possible structures let vfs = null if (pdfFonts.pdfMake?.vfs) { vfs = pdfFonts.pdfMake.vfs } else if (pdfFonts.default?.pdfMake?.vfs) { vfs = pdfFonts.default.pdfMake.vfs } else if (pdfFonts.default?.vfs) { vfs = pdfFonts.default.vfs } else if ((pdfFonts as any).vfs) { vfs = (pdfFonts as any).vfs } else if (pdfFonts.default) { // Check if pdfFonts.default itself is the vfs (contains .ttf files) const defaultKeys = Object.keys(pdfFonts.default) if (defaultKeys.some((key: string) => key.endsWith('.ttf'))) { vfs = pdfFonts.default console.log('Using pdfFonts.default as vfs (contains font files)') } } if (!vfs) { console.error('Available pdfFonts properties:', Object.keys(pdfFonts)) console.error('pdfFonts.default properties:', pdfFonts.default ? Object.keys(pdfFonts.default) : 'N/A') throw new Error('Could not find vfs fonts in pdfmake fonts module') } pdfMakeInstance.vfs = vfs // Format date as DD.MM.YYYY const formatDate = (dateStr: string) => { if (!dateStr) return 'N/A' const date = new Date(dateStr) const day = String(date.getDate()).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, '0') const year = date.getFullYear() return `${day}.${month}.${year}` } // Format currency const formatCurrency = (amount: number) => { return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(amount) } // Group fee details by type const groupedFees: any = { fulfillment: [], return: [], spacerentqm3: [], spacerentflat: [], parcel: [], subscription: [], shippingfee: [] } // Process order lines and collect details reportData.orderLines.forEach((line: any) => { if (line.details && line.details.length > 0) { groupedFees[line.type] = groupedFees[line.type] || [] groupedFees[line.type].push({ productName: line.productName, description: line.description, qtyOrdered: line.qtyOrdered, priceActual: line.priceActual, lineNetAmt: line.lineNetAmt, details: line.details }) } }) // Build table sections for each fee type const feeTypeSections: any[] = [] const feeTypeLabels: any = { fulfillment: 'Fulfillment Fees', return: 'Return Fees', spacerentqm3: 'Space Rent (QM³)', spacerentflat: 'Space Rent (Flat)', parcel: 'Parcel Fees', subscription: 'Subscription Fees', shippingfee: 'Shipping Fees' } Object.keys(groupedFees).forEach((feeType: string) => { const fees = groupedFees[feeType] if (fees.length === 0) return // Add section header feeTypeSections.push({ text: feeTypeLabels[feeType] || feeType.toUpperCase(), style: 'sectionHeader', margin: [0, 15, 0, 5] }) fees.forEach((fee: any) => { // Product summary feeTypeSections.push({ text: `${fee.productName} - ${fee.description}`, style: 'productHeader', margin: [0, 10, 0, 5] }) // Build detail rows const detailRows: any[] = [] // Different columns based on fee type if (feeType === 'fulfillment') { // Header for fulfillment (no Unit Price, add Description after Order) detailRows.push([ { text: 'Fee Line ID', style: 'tableHeader' }, { text: 'Order', style: 'tableHeader' }, { text: 'Description', style: 'tableHeader' }, { text: 'Date', style: 'tableHeader' }, { text: 'Qty', style: 'tableHeader', alignment: 'right' }, { text: 'Amount', style: 'tableHeader', alignment: 'right' } ]) // Data rows fee.details.forEach((detail: any) => { detailRows.push([ { text: `#${detail.id}`, style: 'tableCell' }, { text: detail.orderDocumentNo || 'N/A', style: 'tableCell' }, { text: detail.sourceDescription || '', style: 'tableCell' }, { text: formatDate(detail.shippingDate), style: 'tableCell' }, { text: detail.qty.toFixed(2), style: 'tableCell', alignment: 'right' }, { text: formatCurrency(detail.amount), style: 'tableCell', alignment: 'right' } ]) }) // Subtotal row detailRows.push([ { text: 'Subtotal', style: 'tableCellBold', colSpan: 5, alignment: 'right' }, {}, {}, {}, {}, { text: formatCurrency(fee.lineNetAmt), style: 'tableCellBold', alignment: 'right' } ]) } else if (feeType === 'return') { // Header for return (keep original format) detailRows.push([ { text: 'Fee Line ID', style: 'tableHeader' }, { text: 'Order', style: 'tableHeader' }, { text: 'Date', style: 'tableHeader' }, { text: 'Qty', style: 'tableHeader', alignment: 'right' }, { text: 'Unit Price', style: 'tableHeader', alignment: 'right' }, { text: 'Amount', style: 'tableHeader', alignment: 'right' } ]) // Data rows fee.details.forEach((detail: any) => { detailRows.push([ { text: `#${detail.id}`, style: 'tableCell' }, { text: detail.orderDocumentNo || 'N/A', style: 'tableCell' }, { text: formatDate(detail.shippingDate), style: 'tableCell' }, { text: detail.qty.toFixed(2), style: 'tableCell', alignment: 'right' }, { text: formatCurrency(detail.price), style: 'tableCell', alignment: 'right' }, { text: formatCurrency(detail.amount), style: 'tableCell', alignment: 'right' } ]) }) // Subtotal row detailRows.push([ { text: 'Subtotal', style: 'tableCellBold', colSpan: 5, alignment: 'right' }, {}, {}, {}, {}, { text: formatCurrency(fee.lineNetAmt), style: 'tableCellBold', alignment: 'right' } ]) } else if (feeType === 'shippingfee') { // Header detailRows.push([ { text: 'Fee Line ID', style: 'tableHeader' }, { text: 'Shipment', style: 'tableHeader' }, { text: 'Date', style: 'tableHeader' }, { text: 'Qty', style: 'tableHeader', alignment: 'right' }, { text: 'Unit Price', style: 'tableHeader', alignment: 'right' }, { text: 'Amount', style: 'tableHeader', alignment: 'right' } ]) // Data rows fee.details.forEach((detail: any) => { detailRows.push([ { text: `#${detail.id}`, style: 'tableCell' }, { text: detail.inoutDocumentNo || 'N/A', style: 'tableCell' }, { text: formatDate(detail.shippingDate), style: 'tableCell' }, { text: detail.qty.toFixed(2), style: 'tableCell', alignment: 'right' }, { text: formatCurrency(detail.price), style: 'tableCell', alignment: 'right' }, { text: formatCurrency(detail.amount), style: 'tableCell', alignment: 'right' } ]) }) // Subtotal row detailRows.push([ { text: 'Subtotal', style: 'tableCellBold', colSpan: 5, alignment: 'right' }, {}, {}, {}, {}, { text: formatCurrency(fee.lineNetAmt), style: 'tableCellBold', alignment: 'right' } ]) } else { // Generic fees (space rent, parcel, subscription) // Header detailRows.push([ { text: 'Fee Line ID', style: 'tableHeader' }, { text: 'Date', style: 'tableHeader' }, { text: 'Qty', style: 'tableHeader', alignment: 'right' }, { text: 'Unit Price', style: 'tableHeader', alignment: 'right' }, { text: 'Amount', style: 'tableHeader', alignment: 'right' } ]) // Data rows fee.details.forEach((detail: any) => { detailRows.push([ { text: `#${detail.id}`, style: 'tableCell' }, { text: formatDate(detail.shippingDate), style: 'tableCell' }, { text: detail.qty.toFixed(2), style: 'tableCell', alignment: 'right' }, { text: formatCurrency(detail.price), style: 'tableCell', alignment: 'right' }, { text: formatCurrency(detail.amount), style: 'tableCell', alignment: 'right' } ]) }) // Subtotal row detailRows.push([ { text: 'Subtotal', style: 'tableCellBold', colSpan: 4, alignment: 'right' }, {}, {}, {}, { text: formatCurrency(fee.lineNetAmt), style: 'tableCellBold', alignment: 'right' } ]) } // Add table feeTypeSections.push({ table: { headerRows: 1, widths: feeType === 'fulfillment' ? ['auto', 'auto', '*', 'auto', 'auto', 'auto'] : feeType === 'return' || feeType === 'shippingfee' ? ['auto', '*', 'auto', 'auto', 'auto', 'auto'] : ['auto', '*', 'auto', 'auto', 'auto'], body: detailRows }, layout: { fillColor: (rowIndex: number) => { return rowIndex === 0 ? '#f3f4f6' : (rowIndex % 2 === 0 ? '#fafafa' : null) }, hLineWidth: () => 0.5, vLineWidth: () => 0.5, hLineColor: () => '#e5e7eb', vLineColor: () => '#e5e7eb' }, margin: [0, 5, 0, 10] }) }) }) // Build storage usage section if data exists (ungrouped - all lines) const storageUsageSections: any[] = [] if (reportData.storageUsage && reportData.storageUsage.length > 0) { storageUsageSections.push({ text: 'Storage Usage Details', style: 'sectionHeader', margin: [0, 20, 0, 10] }) // Build single table with all records (not grouped) const storageRows: any[] = [] storageRows.push([ { text: 'Date', style: 'tableHeader' }, { text: 'Product', style: 'tableHeader' }, { text: 'Qty', style: 'tableHeader', alignment: 'right' }, { text: 'Volume Single Price', style: 'tableHeader', alignment: 'right' }, { text: 'Volume (m³)', style: 'tableHeader', alignment: 'right' }, { text: 'Volume Price', style: 'tableHeader', alignment: 'right' } ]) reportData.storageUsage.forEach((record: any) => { storageRows.push([ { text: formatDate(record.dateacct), style: 'tableCell' }, { text: `${record.productvalue || ''} - ${record.product_name || 'Unknown'}`, style: 'tableCell' }, { text: (record.qty || 0).toFixed(0), style: 'tableCell', alignment: 'right' }, { text: (record.volume_single_price || 0).toFixed(2), style: 'tableCell', alignment: 'right' }, { text: (record.volume || 0).toFixed(3), style: 'tableCell', alignment: 'right' }, { text: (record.volume_price || 0).toFixed(2), style: 'tableCell', alignment: 'right' } ]) }) storageUsageSections.push({ table: { headerRows: 1, widths: ['auto', '*', 'auto', 'auto', 'auto', 'auto'], body: storageRows }, layout: { fillColor: (rowIndex: number) => { return rowIndex === 0 ? '#f3f4f6' : (rowIndex % 2 === 0 ? '#fafafa' : null) }, hLineWidth: () => 0.5, vLineWidth: () => 0.5, hLineColor: () => '#e5e7eb', vLineColor: () => '#e5e7eb' }, margin: [0, 5, 0, 10] }) } // Document definition const docDefinition: any = { pageSize: 'A4', pageOrientation: 'portrait', pageMargins: [40, 60, 40, 100], footer: (currentPage: number, pageCount: number) => { return { columns: [ { text: 'The listed prices are not intended for accounting purposes and act only as a reference. They may differ from the invoices.', style: 'disclaimer', width: '*' }, { text: `Page ${currentPage} of ${pageCount}`, style: 'pageNumber', width: 'auto', alignment: 'right' } ], margin: [40, 20, 40, 30] } }, content: [ { text: 'Fulfillment Fee Report', style: 'header', margin: [0, 0, 0, 5] }, { columns: [ { width: '*', stack: [ { text: `Order: ${reportData.documentNo}`, style: 'subheader' }, { text: `Customer: ${reportData.bpartnerName}`, style: 'subheader' } ] }, { width: 'auto', stack: [ { text: `Date: ${formatDate(new Date().toISOString())}`, style: 'subheader', alignment: 'right' } ] } ], margin: [0, 0, 0, 20] }, { canvas: [ { type: 'line', x1: 0, y1: 0, x2: 515, y2: 0, lineWidth: 2, lineColor: '#3b82f6' } ], margin: [0, 0, 0, 20] }, ...feeTypeSections, ...storageUsageSections, { canvas: [ { type: 'line', x1: 0, y1: 0, x2: 515, y2: 0, lineWidth: 2, lineColor: '#3b82f6' } ], margin: [0, 20, 0, 10] }, { columns: [ { text: 'Total Amount:', style: 'totalLabel', width: '*' }, { text: formatCurrency(reportData.totalAmount), style: 'totalAmount', width: 'auto' } ], margin: [0, 10, 0, 0] } ], styles: { header: { fontSize: 22, bold: true, color: '#1e40af' }, subheader: { fontSize: 11, color: '#4b5563' }, sectionHeader: { fontSize: 16, bold: true, color: '#1f2937' }, productHeader: { fontSize: 12, bold: true, color: '#374151' }, tableHeader: { fontSize: 10, bold: true, color: '#374151', fillColor: '#f3f4f6' }, tableCell: { fontSize: 9, color: '#6b7280' }, tableCellBold: { fontSize: 9, bold: true, color: '#111827' }, totalLabel: { fontSize: 14, bold: true, alignment: 'right', color: '#1f2937' }, totalAmount: { fontSize: 16, bold: true, color: '#10b981' }, disclaimer: { fontSize: 9, italics: true, color: '#6b7280', alignment: 'center' }, pageNumber: { fontSize: 9, color: '#6b7280' } }, defaultStyle: { font: 'Roboto' } } // Generate PDF using pdfmake's createPdf method return new Promise((resolve, reject) => { try { const pdfDocGenerator = pdfMakeInstance.createPdf(docDefinition) pdfDocGenerator.getBuffer((buffer: Buffer) => { resolve(buffer) }, (error: any) => { reject(error) }) } catch (error) { reject(error) } }) } const uploadToStrapi = async (pdfBuffer: Buffer, fileName: string) => { const config = useRuntimeConfig() const fs = await import('fs') const path = await import('path') if (!config.api.strapitoken) { throw new Error('STRAPI token not configured in runtime config') } // Save to storage directory (consistent with other PDF storage like invoices, orders, etc.) const storageDir = '/root/storage/fee-reports' if (!fs.existsSync(storageDir)) { fs.mkdirSync(storageDir, { recursive: true }) } const tmpFilePath = path.join(storageDir, fileName) fs.writeFileSync(tmpFilePath, pdfBuffer) // Verify the file was written correctly const fileStats = fs.statSync(tmpFilePath) console.log(`✓ Saved PDF to temporary file: ${tmpFilePath}`) console.log(`✓ File size on disk: ${fileStats.size} bytes (buffer was ${pdfBuffer.length} bytes)`) console.log(`✓ File exists: ${fs.existsSync(tmpFilePath)}`) try { // Use shell command (exactly like your working curl command) const { execSync } = await import('child_process') const strapiUploadBase = config.api.strapiupload || 'http://127.0.0.1:1337' console.log('=== Strapi Upload Debug ===') console.log(`URL: ${strapiUploadBase}/api/upload`) console.log(`File: ${fileName}`) console.log(`File path: ${tmpFilePath}`) console.log(`Size: ${pdfBuffer.length} bytes`) console.log(`Token: ${config.api.strapitoken?.substring(0, 20)}...`) console.log('===========================') const curlCommand = `curl -X POST "${strapiUploadBase}/api/upload" \ -H "Authorization: Bearer ${config.api.strapitoken}" \ -F "files=@${tmpFilePath}" \ -s` console.log('Executing curl command...') const output = execSync(curlCommand, { encoding: 'utf-8' }) console.log('Curl raw output:', output) const uploadResult = JSON.parse(output) console.log(`✓ Upload successful!`) console.log(`✓ Strapi file ID: ${uploadResult[0]?.id}`) console.log(`✓ Strapi hash: ${uploadResult[0]?.hash}`) console.log(`✓ Strapi ext: ${uploadResult[0]?.ext}`) console.log(`✓ File kept for verification: ${tmpFilePath}`) // Return the file reference using hash + ext (consistent with AttachmentModal pattern) if (uploadResult && uploadResult[0] && uploadResult[0].hash && uploadResult[0].ext) { const fileReference = uploadResult[0].hash + uploadResult[0].ext console.log(`✓ File reference: ${fileReference}`) return fileReference } throw new Error('Failed to get file URL from Strapi response') } catch (error: any) { // Fallback: File is already saved in /root/storage/fee-reports console.warn(`⚠️ Strapi upload error: ${error.message}. Using local filename as fallback.`) console.log(`✓ PDF saved locally: ${tmpFilePath}`) // Return just the filename as fallback return fileName } } export default defineEventHandler(async (event) => { try { const body = await readBody(event) const { reportData } = body if (!reportData) { throw new Error('Report data is required') } // Generate PDF const pdfBuffer = await generatePDF(reportData) // Generate shorter filename to avoid POReference field truncation // Use only last 6 digits of timestamp for uniqueness const timestamp = Date.now().toString().slice(-6) const fileName = `fee-${reportData.documentNo}-${timestamp}.pdf` // Upload to Strapi const fileUrl = await uploadToStrapi(pdfBuffer, fileName) return { success: true, fileUrl, fileName } } catch (error: any) { console.error('Error generating fee report:', error) return { success: false, error: error.message || 'Failed to generate fee report' } } })