diff --git a/backend/package-lock.json b/backend/package-lock.json index 0ee4aee..456ce4b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,7 @@ "@google-cloud/documentai": "^9.3.0", "@google-cloud/storage": "^7.16.0", "@supabase/supabase-js": "^2.53.0", + "@types/pdfkit": "^0.17.2", "axios": "^1.11.0", "bcryptjs": "^2.4.3", "cors": "^2.8.5", @@ -26,6 +27,7 @@ "morgan": "^1.10.0", "openai": "^5.10.2", "pdf-parse": "^1.1.1", + "pdfkit": "^0.17.1", "pg": "^8.11.3", "puppeteer": "^21.11.0", "redis": "^4.6.10", @@ -1097,6 +1099,15 @@ "@supabase/storage-js": "^2.10.4" } }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -1276,6 +1287,15 @@ "@types/node": "*" } }, + "node_modules/@types/pdfkit": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.2.tgz", + "integrity": "sha512-a7mqP/l8lsLMVNhQ3N2blU5pA1KX0YFE8FxWp0OTqZQKEZoPk7ndAlW+kdFBAWpFmLpy6fFbMRm4a6ZELWNgOQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/pg": { "version": "8.15.4", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz", @@ -1984,6 +2004,15 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -2159,6 +2188,15 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -2356,6 +2394,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -2437,6 +2481,12 @@ "integrity": "sha512-pM27vqEfxSxRkTMnF+XCmxSEb6duO5R+t8A9DEEJgy4Wz2RVanje2mmj99B6A3zv2r/qGfYlOvYznUhuokizmg==", "license": "BSD-3-Clause" }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -3026,7 +3076,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "devOptional": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -3337,6 +3386,23 @@ } } }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -4140,6 +4206,12 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4295,6 +4367,25 @@ "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4877,6 +4968,12 @@ "node": ">= 14" } }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4991,6 +5088,19 @@ "ms": "^2.1.1" } }, + "node_modules/pdfkit": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.1.tgz", + "integrity": "sha512-Kkf1I9no14O/uo593DYph5u3QwiMfby7JsBSErN1WqeyTgCBNJE3K4pXBn3TgkdKUIVu+buSl4bYUNC+8Up4xg==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^2.0.4", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -5105,6 +5215,11 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -5476,6 +5591,12 @@ "node": ">=4" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -6123,6 +6244,12 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6397,6 +6524,26 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index a210a20..b7d9462 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,6 +28,7 @@ "@google-cloud/documentai": "^9.3.0", "@google-cloud/storage": "^7.16.0", "@supabase/supabase-js": "^2.53.0", + "@types/pdfkit": "^0.17.2", "axios": "^1.11.0", "bcryptjs": "^2.4.3", "cors": "^2.8.5", @@ -42,6 +43,7 @@ "morgan": "^1.10.0", "openai": "^5.10.2", "pdf-parse": "^1.1.1", + "pdfkit": "^0.17.1", "pg": "^8.11.3", "puppeteer": "^21.11.0", "redis": "^4.6.10", @@ -65,4 +67,4 @@ "ts-node-dev": "^2.0.0", "typescript": "^5.2.2" } -} \ No newline at end of file +} diff --git a/backend/src/services/pdfGenerationService.ts b/backend/src/services/pdfGenerationService.ts index 6cfd668..37f0071 100644 --- a/backend/src/services/pdfGenerationService.ts +++ b/backend/src/services/pdfGenerationService.ts @@ -17,6 +17,25 @@ try { }), }; } + +// Import PDFKit for fallback PDF generation +let PDFDocument: any; +try { + PDFDocument = require('pdfkit'); +} catch (error) { + // Mock PDFKit for test environment + PDFDocument = class MockPDFDocument { + constructor() {} + pipe() { return this; } + end() { return this; } + font() { return this; } + fontSize() { return this; } + text() { return this; } + moveDown() { return this; } + addPage() { return this; } + }; +} + import fs from 'fs'; import path from 'path'; import { logger } from '../utils/logger'; @@ -756,106 +775,102 @@ class PDFGenerationService { } /** - * Generate a simple PDF without Chrome (fallback method) + * Generate a simple PDF using PDFKit (fallback method) */ private async generateSimplePDF(analysisData: any): Promise { try { - // Create a simple HTML document that can be converted to PDF - const simpleHTML = this.generateSimpleHTML(analysisData); - - // For now, return a simple text representation - // In a production environment, you might want to use a different PDF library - const textContent = this.convertAnalysisToText(analysisData); - - // Create a simple HTML document - const html = ` - - - - - CIM Review Report - - - -

BLUEPOINT Capital Partners

-

CIM Review Report

-

Generated: ${new Date().toLocaleDateString()} at ${new Date().toLocaleTimeString()}

-
- ${textContent} -
-

BLUEPOINT Capital Partners | CIM Document Processor | Confidential

- - - `; - - // Try to generate PDF with Puppeteer again, but with different options - try { - const page = await this.getPage(); - await page.setContent(html, { waitUntil: 'networkidle0' }); - const buffer = await page.pdf({ - format: 'A4', - margin: { top: '0.5in', right: '0.5in', bottom: '0.5in', left: '0.5in' }, - printBackground: true, + return new Promise((resolve, reject) => { + const doc = new PDFDocument({ + size: 'A4', + margins: { + top: 50, + bottom: 50, + left: 50, + right: 50 + } }); - this.releasePage(page); - return buffer; - } catch (puppeteerError) { - logger.error('Puppeteer fallback also failed, returning error response', puppeteerError); - throw new Error('PDF generation is currently unavailable. Please try again later.'); - } + + const chunks: Buffer[] = []; + doc.on('data', (chunk: Buffer) => chunks.push(chunk)); + doc.on('end', () => { + const result = Buffer.concat(chunks); + resolve(result); + }); + doc.on('error', (error: any) => { + reject(error); + }); + + // Add header + doc.fontSize(24) + .font('Helvetica-Bold') + .text('BLUEPOINT Capital Partners', { align: 'center' }); + + doc.moveDown(0.5); + doc.fontSize(18) + .font('Helvetica-Bold') + .text('CIM Review Report', { align: 'center' }); + + doc.moveDown(0.5); + doc.fontSize(10) + .font('Helvetica') + .text(`Generated: ${new Date().toLocaleDateString()} at ${new Date().toLocaleTimeString()}`, { align: 'center' }); + + doc.moveDown(2); + + // Add content sections + const sections = [ + { title: 'Deal Overview', data: analysisData.dealOverview }, + { title: 'Business Description', data: analysisData.businessDescription }, + { title: 'Market & Industry Analysis', data: analysisData.marketIndustryAnalysis }, + { title: 'Financial Summary', data: analysisData.financialSummary }, + { title: 'Management Team Overview', data: analysisData.managementTeamOverview }, + { title: 'Preliminary Investment Thesis', data: analysisData.preliminaryInvestmentThesis }, + { title: 'Key Questions & Next Steps', data: analysisData.keyQuestionsNextSteps }, + ]; + + sections.forEach(section => { + if (section.data) { + // Add section title + doc.fontSize(14) + .font('Helvetica-Bold') + .text(section.title); + + doc.moveDown(0.5); + + // Add section content + Object.entries(section.data).forEach(([key, value]) => { + if (value && typeof value !== 'object') { + doc.fontSize(10) + .font('Helvetica-Bold') + .text(`${this.formatFieldName(key)}:`, { continued: true }); + + doc.fontSize(10) + .font('Helvetica') + .text(` ${value}`); + + doc.moveDown(0.3); + } + }); + + doc.moveDown(1); + } + }); + + // Add footer + doc.moveDown(2); + doc.fontSize(8) + .font('Helvetica') + .text('BLUEPOINT Capital Partners | CIM Document Processor | Confidential', { align: 'center' }); + + doc.end(); + }); } catch (error) { - logger.error('Simple PDF generation failed', error); + logger.error('PDFKit PDF generation failed', error); throw error; } } - /** - * Convert analysis data to simple text format - */ - private convertAnalysisToText(analysisData: any): string { - let text = ''; - - const sections = [ - { title: 'Deal Overview', data: analysisData.dealOverview }, - { title: 'Business Description', data: analysisData.businessDescription }, - { title: 'Market & Industry Analysis', data: analysisData.marketIndustryAnalysis }, - { title: 'Financial Summary', data: analysisData.financialSummary }, - { title: 'Management Team Overview', data: analysisData.managementTeamOverview }, - { title: 'Preliminary Investment Thesis', data: analysisData.preliminaryInvestmentThesis }, - { title: 'Key Questions & Next Steps', data: analysisData.keyQuestionsNextSteps }, - ]; - sections.forEach(section => { - if (section.data) { - text += `

${section.title}

`; - - Object.entries(section.data).forEach(([key, value]) => { - if (value && typeof value !== 'object') { - text += `
${this.formatFieldName(key)}:${value}
`; - } - }); - - text += '
'; - } - }); - - return text; - } - - /** - * Generate simple HTML for fallback PDF generation - */ - private generateSimpleHTML(analysisData: any): string { - return this.convertAnalysisToText(analysisData); - } /** * Generate HTML from CIM Review analysis data