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.
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');
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)
22function authenticatePdfRequest(req, res, next) {
23 // Check for token in cookie first (most secure - automatic from browser)
24 let token = req.cookies.auth_token;
26 // If not in cookie, check Authorization header (for API calls)
28 const authHeader = req.headers['authorization'];
29 token = authHeader && authHeader.split(' ')[1];
32 // Legacy fallback: query parameter (less secure, but backwards compatible)
34 token = req.query.token;
38 return res.status(401).send('Access denied - No token provided');
41 // Verify token with JWT_SECRET
42 jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
45 req.user.tenantId = user.tenant_id || null;
49 // Try AGENT_SIGN_KEY as fallback
50 jwt.verify(token, process.env.AGENT_SIGN_KEY, (err2, user2) => {
53 req.user.tenantId = user2.tenant_id || null;
56 return res.status(403).send('Invalid token');
62 * @api {get} /invoices/:id/pdf Generate and Download Invoice PDF
63 * @apiName GenerateInvoicePDF
65 * @apiDescription Generates a PDF for the invoice and returns it for download/viewing
67router.get('/:id/pdf', authenticatePdfRequest, async (req, res) => {
68 const { id } = req.params;
69 const { download = 'false' } = req.query;
72 console.log('[PDF] Starting PDF generation for invoice:', id);
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}` : '';
79 console.log('[PDF] Fetching invoice with params:', baseParams);
80 const invoiceResult = await pool.query(`
83 WHERE i.invoice_id = $1 ${where}
86 if (invoiceResult.rows.length === 0) {
87 console.log('[PDF] Invoice not found:', id);
88 return res.status(404).json({ error: 'Invoice not found' });
91 const invoice = invoiceResult.rows[0];
92 console.log('[PDF] Invoice found:', invoice.invoice_id);
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',
100 console.log('[PDF] Found', itemsResult.rows.length, 'line items');
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]
108 const customer = customerResult.rows[0];
109 console.log('[PDF] Customer found:', customer?.customer_id);
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'
120 console.log('[PDF] Company info:', companyInfo);
122 // Delete old PDFs for this invoice
123 console.log('[PDF] Deleting old PDFs');
124 await deleteInvoicePDFs(invoice.invoice_id);
127 console.log('[PDF] Calling generateInvoicePDF');
128 const pdfPath = await generateInvoicePDF({
132 items: itemsResult.rows
134 console.log('[PDF] PDF generated at:', pdfPath);
137 const filename = `invoice-${String(invoice.invoice_id).padStart(5, '0')}.pdf`;
139 res.setHeader('Content-Type', 'application/pdf');
141 if (download === 'true') {
142 res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
144 res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
148 const stream = fs.createReadStream(pdfPath);
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' });
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);
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,
173 res.status(500).json({ error: 'Failed to generate PDF', details: error.message });
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)
183router.get('/public/:token/pdf', async (req, res) => {
184 const { token } = req.params;
185 const { download = 'false' } = req.query;
189 if (!/^[0-9a-f]{64}$/i.test(token)) {
190 return res.status(400).json({ error: 'Invalid payment token format' });
193 // Fetch invoice by payment token
194 const invoiceResult = await pool.query(`
197 WHERE i.payment_token = $1
200 if (invoiceResult.rows.length === 0) {
201 return res.status(404).json({ error: 'Invoice not found' });
204 const invoice = invoiceResult.rows[0];
207 const itemsResult = await pool.query(
208 'SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_id',
213 const customerResult = await pool.query(
214 'SELECT * FROM customers WHERE customer_id = $1',
215 [invoice.customer_id]
217 const customer = customerResult.rows[0];
219 // Fetch company info
220 const companyResult = await pool.query(`
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
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'
238 await deleteInvoicePDFs(invoice.invoice_id);
241 const pdfPath = await generateInvoicePDF({
245 items: itemsResult.rows
249 const filename = `invoice-${String(invoice.invoice_id).padStart(5, '0')}.pdf`;
251 res.setHeader('Content-Type', 'application/pdf');
253 if (download === 'true') {
254 res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
256 res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
260 const stream = fs.createReadStream(pdfPath);
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' });
271 console.error('Error generating public invoice PDF:', error);
272 res.status(500).json({ error: 'Failed to generate PDF', details: error.message });
276module.exports = router;