EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
invoice-pdf.js
Go to the documentation of this file.
1/**
2 * @file routes/invoice-pdf.js
3 * @module routes/invoice-pdf
4 * @description Invoice PDF generation and serving endpoints.
5 * Generates professional PDFs using server-side templates.
6 */
7
8const express = require('express');
9const router = express.Router();
10const path = require('path');
11const fs = require('fs');
12const jwt = require('jsonwebtoken');
13const pool = require('../services/db');
14const authenticateToken = require('../middleware/auth');
15const { generateInvoicePDF, deleteInvoicePDFs, PDFS_DIR } = require('../services/pdfGenerator');
16const { getTenantFilter } = require('../middleware/tenant');
17
18/**
19 * Custom authentication for PDF routes - accepts token from cookie, header, OR query parameter
20 * Priority: 1. Cookie (preferred - secure), 2. Authorization header, 3. Query param (fallback)
21 */
22function authenticatePdfRequest(req, res, next) {
23 // Check for token in cookie first (most secure - automatic from browser)
24 let token = req.cookies.auth_token;
25
26 // If not in cookie, check Authorization header (for API calls)
27 if (!token) {
28 const authHeader = req.headers['authorization'];
29 token = authHeader && authHeader.split(' ')[1];
30 }
31
32 // Legacy fallback: query parameter (less secure, but backwards compatible)
33 if (!token) {
34 token = req.query.token;
35 }
36
37 if (!token) {
38 return res.status(401).send('Access denied - No token provided');
39 }
40
41 // Verify token with JWT_SECRET
42 jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
43 if (!err) {
44 req.user = user;
45 req.user.tenantId = user.tenant_id || null;
46 return next();
47 }
48
49 // Try AGENT_SIGN_KEY as fallback
50 jwt.verify(token, process.env.AGENT_SIGN_KEY, (err2, user2) => {
51 if (!err2) {
52 req.user = user2;
53 req.user.tenantId = user2.tenant_id || null;
54 return next();
55 }
56 return res.status(403).send('Invalid token');
57 });
58 });
59}
60
61/**
62 * @api {get} /invoices/:id/pdf Generate and Download Invoice PDF
63 * @apiName GenerateInvoicePDF
64 * @apiGroup Invoices
65 * @apiDescription Generates a PDF for the invoice and returns it for download/viewing
66 */
67router.get('/:id/pdf', authenticatePdfRequest, async (req, res) => {
68 const { id } = req.params;
69 const { download = 'false' } = req.query;
70
71 try {
72 console.log('[PDF] Starting PDF generation for invoice:', id);
73
74 // Fetch invoice with tenant filtering
75 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req, 'i');
76 const baseParams = [id, ...tenantParams];
77 const where = tenantClause ? `AND ${tenantClause}` : '';
78
79 console.log('[PDF] Fetching invoice with params:', baseParams);
80 const invoiceResult = await pool.query(`
81 SELECT i.*
82 FROM invoices i
83 WHERE i.invoice_id = $1 ${where}
84 `, baseParams);
85
86 if (invoiceResult.rows.length === 0) {
87 console.log('[PDF] Invoice not found:', id);
88 return res.status(404).json({ error: 'Invoice not found' });
89 }
90
91 const invoice = invoiceResult.rows[0];
92 console.log('[PDF] Invoice found:', invoice.invoice_id);
93
94 // Fetch line items
95 console.log('[PDF] Fetching line items');
96 const itemsResult = await pool.query(
97 'SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_id',
98 [id]
99 );
100 console.log('[PDF] Found', itemsResult.rows.length, 'line items');
101
102 // Fetch customer
103 console.log('[PDF] Fetching customer:', invoice.customer_id);
104 const customerResult = await pool.query(
105 'SELECT * FROM customers WHERE customer_id = $1',
106 [invoice.customer_id]
107 );
108 const customer = customerResult.rows[0];
109 console.log('[PDF] Customer found:', customer?.customer_id);
110
111 // Company info - use hardcoded fallback values (settings table doesn't exist yet)
112 console.log('[PDF] Using fallback company info');
113 const companyInfo = {
114 name: 'Independent Business Group',
115 abn: '12 345 678 901',
116 address: '123 Business Street, Sydney NSW 2000',
117 phone: '1300 123 456',
118 email: 'accounts@ibg.com.au'
119 };
120 console.log('[PDF] Company info:', companyInfo);
121
122 // Delete old PDFs for this invoice
123 console.log('[PDF] Deleting old PDFs');
124 await deleteInvoicePDFs(invoice.invoice_id);
125
126 // Generate new PDF
127 console.log('[PDF] Calling generateInvoicePDF');
128 const pdfPath = await generateInvoicePDF({
129 invoice,
130 customer,
131 companyInfo,
132 items: itemsResult.rows
133 });
134 console.log('[PDF] PDF generated at:', pdfPath);
135
136 // Set headers
137 const filename = `invoice-${String(invoice.invoice_id).padStart(5, '0')}.pdf`;
138
139 res.setHeader('Content-Type', 'application/pdf');
140
141 if (download === 'true') {
142 res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
143 } else {
144 res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
145 }
146
147 // Stream the PDF
148 const stream = fs.createReadStream(pdfPath);
149 stream.pipe(res);
150
151 stream.on('error', (err) => {
152 console.error('Error streaming PDF:', err);
153 if (!res.headersSent) {
154 res.status(500).json({ error: 'Error serving PDF' });
155 }
156 });
157
158 } catch (error) {
159 console.error('Error generating invoice PDF:', error);
160 console.error('Error stack:', error.stack);
161 console.error('Invoice ID:', req.params.id);
162 console.error('User:', req.user);
163
164 // Send detailed error in development
165 if (process.env.NODE_ENV !== 'production') {
166 return res.status(500).json({
167 error: 'Failed to generate PDF',
168 details: error.message,
169 stack: error.stack
170 });
171 }
172
173 res.status(500).json({ error: 'Failed to generate PDF', details: error.message });
174 }
175});
176
177/**
178 * @api {get} /public/pay/:token/pdf Generate Invoice PDF (Public)
179 * @apiName GeneratePublicInvoicePDF
180 * @apiGroup PublicPayment
181 * @apiDescription Generates PDF for public invoice (no auth required)
182 */
183router.get('/public/:token/pdf', async (req, res) => {
184 const { token } = req.params;
185 const { download = 'false' } = req.query;
186
187 try {
188 // Validate token
189 if (!/^[0-9a-f]{64}$/i.test(token)) {
190 return res.status(400).json({ error: 'Invalid payment token format' });
191 }
192
193 // Fetch invoice by payment token
194 const invoiceResult = await pool.query(`
195 SELECT i.*
196 FROM invoices i
197 WHERE i.payment_token = $1
198 `, [token]);
199
200 if (invoiceResult.rows.length === 0) {
201 return res.status(404).json({ error: 'Invoice not found' });
202 }
203
204 const invoice = invoiceResult.rows[0];
205
206 // Fetch line items
207 const itemsResult = await pool.query(
208 'SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_id',
209 [invoice.invoice_id]
210 );
211
212 // Fetch customer
213 const customerResult = await pool.query(
214 'SELECT * FROM customers WHERE customer_id = $1',
215 [invoice.customer_id]
216 );
217 const customer = customerResult.rows[0];
218
219 // Fetch company info
220 const companyResult = await pool.query(`
221 SELECT
222 (SELECT value FROM settings WHERE key = 'company_name' LIMIT 1) as name,
223 (SELECT value FROM settings WHERE key = 'company_abn' LIMIT 1) as abn,
224 (SELECT value FROM settings WHERE key = 'company_address' LIMIT 1) as address,
225 (SELECT value FROM settings WHERE key = 'company_phone' LIMIT 1) as phone,
226 (SELECT value FROM settings WHERE key = 'company_email' LIMIT 1) as email
227 `);
228
229 const companyInfo = {
230 name: companyResult.rows[0]?.name || 'Independent Business Group',
231 abn: companyResult.rows[0]?.abn || '12 345 678 901',
232 address: companyResult.rows[0]?.address || '123 Business Street, Sydney NSW 2000',
233 phone: companyResult.rows[0]?.phone || '1300 123 456',
234 email: companyResult.rows[0]?.email || 'accounts@ibg.com.au'
235 };
236
237 // Delete old PDFs
238 await deleteInvoicePDFs(invoice.invoice_id);
239
240 // Generate PDF
241 const pdfPath = await generateInvoicePDF({
242 invoice,
243 customer,
244 companyInfo,
245 items: itemsResult.rows
246 });
247
248 // Set headers
249 const filename = `invoice-${String(invoice.invoice_id).padStart(5, '0')}.pdf`;
250
251 res.setHeader('Content-Type', 'application/pdf');
252
253 if (download === 'true') {
254 res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
255 } else {
256 res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
257 }
258
259 // Stream the PDF
260 const stream = fs.createReadStream(pdfPath);
261 stream.pipe(res);
262
263 stream.on('error', (err) => {
264 console.error('Error streaming PDF:', err);
265 if (!res.headersSent) {
266 res.status(500).json({ error: 'Error serving PDF' });
267 }
268 });
269
270 } catch (error) {
271 console.error('Error generating public invoice PDF:', error);
272 res.status(500).json({ error: 'Failed to generate PDF', details: error.message });
273 }
274});
275
276module.exports = router;