REDROOM
PHP 8.2.30
Path:
Logout
Edit File
Size: 25.39 KB
Close
//home/byroehnu/easepay.easetack.com/controllers/invoiceController.js
Text
Base64
const { prisma } = require('../config/prisma'); const QRCode = require('qrcode'); const PDFDocument = require('pdfkit'); const fs = require('fs'); const path = require('path'); const invoiceController = { // Get next invoice number getNextInvoiceNumber: async (req, res) => { try { const userId = req.user.id; const businessId = req.user.businessId; let profile; if (businessId) { profile = await prisma.businessProfile.findUnique({ where: { businessId } }); } // If no profile exists, create default one if (!profile && businessId) { profile = await prisma.businessProfile.create({ data: { businessId, nextInvoiceNumber: 1 } }); } const nextNumber = profile ? profile.nextInvoiceNumber : 1; const prefix = profile?.invoicePrefix || 'INV'; res.json({ success: true, data: { nextInvoiceNumber: `${prefix}-${String(nextNumber).padStart(5, '0')}` } }); } catch (error) { console.error('Error getting next invoice number:', error); res.status(500).json({ success: false, message: 'Error getting next invoice number', error: error.message }); } }, // Create new invoice createInvoice: async (req, res) => { try { const userId = req.user.id; const businessId = req.user.businessId; const { customerName, customerEmail, customerPhone, customerAddress, invoiceNumber, invoiceDate, dueDate, paymentMethod, items, subtotal, discount, discountType = 'FIXED', vatRate, vatAmount, total, notes, currency = 'NGN' } = req.body; // Validate required fields if (!customerName || !items || items.length === 0 || !total) { return res.status(400).json({ success: false, message: 'Missing required fields: customerName, items, and total are required' }); } // Create invoice const invoice = await prisma.invoice.create({ data: { invoiceNumber, customerName, customerEmail, customerPhone, customerAddress, businessId, userId, invoiceDate: invoiceDate ? new Date(invoiceDate) : new Date(), dueDate: dueDate ? new Date(dueDate) : null, paymentMethod, subtotal: parseFloat(subtotal), discount: parseFloat(discount) || 0, discountType, vatRate: parseFloat(vatRate) || 0, vatAmount: parseFloat(vatAmount) || 0, total: parseFloat(total), notes, currency, status: 'CREATED', items: { create: items.map(item => ({ itemName: item.name, description: item.description, quantity: parseFloat(item.quantity), unitPrice: parseFloat(item.unitPrice), total: parseFloat(item.total), inventoryItemId: item.inventoryItemId || null })) } }, include: { items: true, business: true } }); // Generate QR Code const shareUrl = `${process.env.FRONTEND_URL}/invoice/${invoice.id}`; const qrCodeData = await QRCode.toDataURL(shareUrl); // Update invoice with QR code const updatedInvoice = await prisma.invoice.update({ where: { id: invoice.id }, data: { qrCodeUrl: shareUrl, qrCodeData, shareUrl, downloadUrl: `${process.env.API_URL}/api/invoices/${invoice.id}/pdf` } }); // Update next invoice number if (businessId) { const profile = await prisma.businessProfile.findUnique({ where: { businessId } }); if (profile) { await prisma.businessProfile.update({ where: { businessId }, data: { nextInvoiceNumber: profile.nextInvoiceNumber + 1 } }); } } // Update inventory if items are linked for (const item of items) { if (item.inventoryItemId) { await prisma.inventoryItem.update({ where: { id: item.inventoryItemId }, data: { stock: { decrement: parseFloat(item.quantity) } } }); } } res.status(201).json({ success: true, message: 'Invoice created successfully', data: { id: updatedInvoice.id, invoiceNumber: updatedInvoice.invoiceNumber, status: updatedInvoice.status, qrCode: updatedInvoice.qrCodeData, downloadUrl: updatedInvoice.downloadUrl, shareUrl: updatedInvoice.shareUrl, total: updatedInvoice.total, createdAt: updatedInvoice.createdAt } }); } catch (error) { console.error('Error creating invoice:', error); res.status(500).json({ success: false, message: 'Error creating invoice', error: error.message }); } }, // Get all invoices getAllInvoices: async (req, res) => { try { const userId = req.user.id; const businessId = req.user.businessId; const { page = 1, limit = 10, status, startDate, endDate } = req.query; const whereClause = { userId, ...(businessId && { businessId }), ...(status && { status }), ...(startDate && endDate && { invoiceDate: { gte: new Date(startDate), lte: new Date(endDate) } }) }; const invoices = await prisma.invoice.findMany({ where: whereClause, include: { items: true, business: true }, orderBy: { createdAt: 'desc' }, skip: (parseInt(page) - 1) * parseInt(limit), take: parseInt(limit) }); const totalInvoices = await prisma.invoice.count({ where: whereClause }); res.json({ success: true, data: { invoices, pagination: { currentPage: parseInt(page), totalPages: Math.ceil(totalInvoices / parseInt(limit)), totalInvoices, limit: parseInt(limit) } } }); } catch (error) { console.error('Error getting invoices:', error); res.status(500).json({ success: false, message: 'Error getting invoices', error: error.message }); } }, // Get single invoice getInvoice: async (req, res) => { try { const { id } = req.params; const userId = req.user?.id; // Optional for public invoice view const invoice = await prisma.invoice.findFirst({ where: { id, ...(userId && { userId }) }, include: { items: true, business: { include: { profile: true } }, payments: true } }); if (!invoice) { return res.status(404).json({ success: false, message: 'Invoice not found' }); } res.json({ success: true, data: invoice }); } catch (error) { console.error('Error getting invoice:', error); res.status(500).json({ success: false, message: 'Error getting invoice', error: error.message }); } }, // Update invoice updateInvoice: async (req, res) => { try { const { id } = req.params; const userId = req.user.id; const updateData = req.body; // Check if invoice exists and belongs to user const existingInvoice = await prisma.invoice.findFirst({ where: { id, userId } }); if (!existingInvoice) { return res.status(404).json({ success: false, message: 'Invoice not found' }); } // Prevent updating paid invoices if (existingInvoice.status === 'PAID') { return res.status(400).json({ success: false, message: 'Cannot update paid invoice' }); } const updatedInvoice = await prisma.invoice.update({ where: { id }, data: updateData, include: { items: true, business: true } }); res.json({ success: true, message: 'Invoice updated successfully', data: updatedInvoice }); } catch (error) { console.error('Error updating invoice:', error); res.status(500).json({ success: false, message: 'Error updating invoice', error: error.message }); } }, // Delete invoice deleteInvoice: async (req, res) => { try { const { id } = req.params; const userId = req.user.id; const invoice = await prisma.invoice.findFirst({ where: { id, userId } }); if (!invoice) { return res.status(404).json({ success: false, message: 'Invoice not found' }); } await prisma.invoice.delete({ where: { id } }); res.json({ success: true, message: 'Invoice deleted successfully' }); } catch (error) { console.error('Error deleting invoice:', error); res.status(500).json({ success: false, message: 'Error deleting invoice', error: error.message }); } }, // Generate PDF (Legacy method - kept for backward compatibility) generateInvoice: async (req, res) => { try { const { senderDetails, clientDetails, items, invoiceNumber, date, dueDate, tax, total, notes } = req.body; const doc = new PDFDocument({ margin: 50, size: 'A4' }); // Stream the PDF to the response res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename=invoice-${invoiceNumber || 'draft'}.pdf`); doc.pipe(res); // --- Header Section --- generateHeader(doc, senderDetails); // --- Client & Invoice Info --- generateCustomerInformation(doc, clientDetails, invoiceNumber, date, dueDate); // --- Items Table --- generateInvoiceTable(doc, items, tax, total); // --- Footer --- generateFooter(doc, notes); doc.end(); } catch (error) { console.error('Error generating invoice:', error); if (!res.headersSent) { res.status(500).json({ error: 'Failed to generate invoice' }); } } }, // Generate PDF for specific invoice generatePDF: async (req, res) => { try { const { id } = req.params; const userId = req.user?.id; const invoice = await prisma.invoice.findFirst({ where: { id, ...(userId && { userId }) }, include: { items: true, business: { include: { profile: true } } } }); if (!invoice) { return res.status(404).json({ success: false, message: 'Invoice not found' }); } // Create PDF const doc = new PDFDocument(); let filename = `invoice-${invoice.invoiceNumber}.pdf`; res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); doc.pipe(res); // PDF Content doc.fontSize(20).text('INVOICE', 50, 50); doc.fontSize(12).text(`Invoice #: ${invoice.invoiceNumber}`, 50, 80); doc.text(`Date: ${invoice.invoiceDate.toDateString()}`, 50, 100); // Business details if (invoice.business?.profile) { const profile = invoice.business.profile; doc.text(`From: ${profile.registrationName || invoice.business.name}`, 50, 140); if (profile.email) doc.text(`Email: ${profile.email}`, 50, 160); if (profile.phone) doc.text(`Phone: ${profile.phone}`, 50, 180); if (profile.address) doc.text(`Address: ${profile.address}`, 50, 200); } // Customer details doc.text(`Bill To: ${invoice.customerName}`, 300, 140); if (invoice.customerEmail) doc.text(`Email: ${invoice.customerEmail}`, 300, 160); if (invoice.customerPhone) doc.text(`Phone: ${invoice.customerPhone}`, 300, 180); // Items table let yPosition = 240; doc.text('Description', 50, yPosition); doc.text('Qty', 200, yPosition); doc.text('Price', 250, yPosition); doc.text('Total', 300, yPosition); yPosition += 20; invoice.items.forEach(item => { doc.text(item.itemName, 50, yPosition); doc.text(item.quantity.toString(), 200, yPosition); doc.text(`${invoice.currency} ${item.unitPrice}`, 250, yPosition); doc.text(`${invoice.currency} ${item.total}`, 300, yPosition); yPosition += 20; }); // Totals yPosition += 20; doc.text(`Subtotal: ${invoice.currency} ${invoice.subtotal}`, 300, yPosition); if (invoice.discount > 0) { yPosition += 20; doc.text(`Discount: ${invoice.currency} ${invoice.discount}`, 300, yPosition); } if (invoice.vatAmount > 0) { yPosition += 20; doc.text(`VAT (${invoice.vatRate}%): ${invoice.currency} ${invoice.vatAmount}`, 300, yPosition); } yPosition += 20; doc.fontSize(14).text(`Total: ${invoice.currency} ${invoice.total}`, 300, yPosition); // Notes if (invoice.notes) { yPosition += 40; doc.fontSize(12).text('Notes:', 50, yPosition); doc.text(invoice.notes, 50, yPosition + 20); } doc.end(); } catch (error) { console.error('Error generating PDF:', error); res.status(500).json({ success: false, message: 'Error generating PDF', error: error.message }); } }, // Send invoice via email sendInvoiceEmail: async (req, res) => { try { const { id } = req.params; const { recipientEmail, message } = req.body; const userId = req.user.id; const invoice = await prisma.invoice.findFirst({ where: { id, userId }, include: { business: { include: { profile: true } } } }); if (!invoice) { return res.status(404).json({ success: false, message: 'Invoice not found' }); } // Here you would integrate with your email service // For now, just update invoice status await prisma.invoice.update({ where: { id }, data: { status: 'SENT' } }); res.json({ success: true, message: 'Invoice sent successfully', data: { emailSent: true, sentAt: new Date() } }); } catch (error) { console.error('Error sending invoice email:', error); res.status(500).json({ success: false, message: 'Error sending invoice email', error: error.message }); } }, // Get QR code for invoice getQRCode: async (req, res) => { try { const { id } = req.params; const userId = req.user?.id; const invoice = await prisma.invoice.findFirst({ where: { id, ...(userId && { userId }) } }); if (!invoice) { return res.status(404).json({ success: false, message: 'Invoice not found' }); } const qrData = { qrCodeUrl: invoice.qrCodeUrl, qrCodeData: invoice.qrCodeData, paymentUrl: invoice.shareUrl }; res.json({ success: true, data: qrData }); } catch (error) { console.error('Error getting QR code:', error); res.status(500).json({ success: false, message: 'Error getting QR code', error: error.message }); } }, // Get invoice analytics getInvoiceAnalytics: async (req, res) => { try { const userId = req.user.id; const businessId = req.user.businessId; const { period = 'month' } = req.query; let dateFilter = {}; const now = new Date(); switch (period) { case 'week': dateFilter = { gte: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7) }; break; case 'month': dateFilter = { gte: new Date(now.getFullYear(), now.getMonth(), 1) }; break; case 'year': dateFilter = { gte: new Date(now.getFullYear(), 0, 1) }; break; } const whereClause = { userId, ...(businessId && { businessId }), ...(Object.keys(dateFilter).length && { invoiceDate: dateFilter }) }; const analytics = await prisma.invoice.aggregate({ where: whereClause, _count: { id: true }, _sum: { total: true, paidAmount: true } }); const statusBreakdown = await prisma.invoice.groupBy({ by: ['status'], where: whereClause, _count: { status: true }, _sum: { total: true } }); res.json({ success: true, data: { totalInvoices: analytics._count.id, totalValue: analytics._sum.total || 0, totalPaid: analytics._sum.paidAmount || 0, statusBreakdown, period } }); } catch (error) { console.error('Error getting invoice analytics:', error); res.status(500).json({ success: false, message: 'Error getting invoice analytics', error: error.message }); } } }; function generateHeader(doc, sender) { // doc.image('logo.png', 50, 45, { width: 50 }) // .fillColor('#444444') // .fontSize(20) // .text(sender.name || 'Company Name', 110, 57) // .fontSize(10) // .text(sender.address || '', 200, 65, { align: 'right' }) // .text(sender.city || '', 200, 80, { align: 'right' }) // .moveDown(); doc .fillColor('#444444') .fontSize(20) .text(sender?.name || 'EasePay', 50, 57) .fontSize(10) .text(sender?.name || 'EasePay Inc.', 200, 50, { align: 'right' }) .text(sender?.address || '123 Business Rd', 200, 65, { align: 'right' }) .text(`${sender?.city || 'City'}, ${sender?.country || 'Country'}`, 200, 80, { align: 'right' }) .moveDown(); // Draw a line under header generateHr(doc, 100); } function generateCustomerInformation(doc, client, invoiceNumber, date, dueDate) { doc .fillColor('#444444') .fontSize(20) .text('Invoice', 50, 160); generateHr(doc, 185); const customerInformationTop = 200; doc .fontSize(10) .text('Invoice Number:', 50, customerInformationTop) .font('Helvetica-Bold') .text(invoiceNumber || 'INV-001', 150, customerInformationTop) .font('Helvetica') .text('Invoice Date:', 50, customerInformationTop + 15) .text(date || new Date().toISOString().split('T')[0], 150, customerInformationTop + 15) .text('Balance Due:', 50, customerInformationTop + 30) .text(formatCurrency(0), 150, customerInformationTop + 30) // Assuming paid or partial logic later .font('Helvetica-Bold') .text(client?.name || 'Client Name', 300, customerInformationTop) .font('Helvetica') .text(client?.address || 'Client Address', 300, customerInformationTop + 15) .text( `${client?.city || 'City'}, ${client?.country || 'Country'}`, 300, customerInformationTop + 30 ) .moveDown(); generateHr(doc, 252); } function generateInvoiceTable(doc, items, tax, total) { let i; const invoiceTableTop = 330; doc.font('Helvetica-Bold'); generateTableRow( doc, invoiceTableTop, 'Item', 'Description', 'Unit Cost', 'Quantity', 'Line Total' ); generateHr(doc, invoiceTableTop + 20); doc.font('Helvetica'); let position = 0; // Fallback if no items const invoiceItems = items && items.length > 0 ? items : [ { name: 'Service A', description: 'Consulting', amount: 100, quantity: 2 }, { name: 'Service B', description: 'Development', amount: 50, quantity: 5 } ]; for (i = 0; i < invoiceItems.length; i++) { const item = invoiceItems[i]; position = invoiceTableTop + (i + 1) * 30; generateTableRow( doc, position, item.name, item.description, formatCurrency(item.amount), item.quantity, formatCurrency(item.amount * item.quantity) ); generateHr(doc, position + 20); } const subtotalPosition = invoiceTableTop + (invoiceItems.length + 1) * 30; generateTableRow( doc, subtotalPosition, '', '', 'Subtotal', '', formatCurrency(total || 0) // Should calculate sum if total not provided ); } // Helper functions for legacy PDF generation function generateHeader(doc, sender) { doc .fillColor('#444444') .fontSize(20) .text(sender?.name || 'Your Company', 50, 57) .fontSize(10) .text(sender?.name || 'Your Company', 200, 50, { align: 'right' }) .text(sender?.address || '123 Main Street', 200, 65, { align: 'right' }) .text(`${sender?.city || 'New York'}, ${sender?.state || 'NY'} ${sender?.country || 'USA'}`, 200, 80, { align: 'right' }) .moveDown(); } function generateCustomerInformation(doc, customer, invoiceNumber, date, dueDate) { doc .fillColor('#444444') .fontSize(20) .text('Invoice', 50, 160); generateHr(doc, 185); const customerInformationTop = 200; doc .fontSize(10) .text('Invoice Number:', 50, customerInformationTop) .font('Helvetica-Bold') .text(invoiceNumber || 'INV-001', 150, customerInformationTop) .font('Helvetica') .text('Invoice Date:', 50, customerInformationTop + 15) .text(date || new Date().toLocaleDateString(), 150, customerInformationTop + 15) .text('Due Date:', 50, customerInformationTop + 30) .text(dueDate || 'N/A', 150, customerInformationTop + 30) .font('Helvetica-Bold') .text('Bill To:', 300, customerInformationTop) .font('Helvetica') .text(customer?.name || 'Customer Name', 300, customerInformationTop + 15) .text(customer?.address || 'Customer Address', 300, customerInformationTop + 30) .text(`${customer?.city || 'City'}, ${customer?.state || 'State'} ${customer?.country || 'Country'}`, 300, customerInformationTop + 45) .moveDown(); generateHr(doc, 252); } function generateInvoiceTable(doc, items, tax, total) { let i; const invoiceTableTop = 330; doc.font('Helvetica-Bold'); generateTableRow( doc, invoiceTableTop, 'Item', 'Description', 'Unit Price', 'Quantity', 'Line Total' ); generateHr(doc, invoiceTableTop + 20); doc.font('Helvetica'); for (i = 0; i < (items?.length || 0); i++) { const item = items[i]; const position = invoiceTableTop + (i + 1) * 30; generateTableRow( doc, position, item?.name || 'Item', item?.description || 'Description', formatCurrency(item?.rate || 0), item?.quantity || 1, formatCurrency((item?.rate || 0) * (item?.quantity || 1)) ); generateHr(doc, position + 20); } const subtotalPosition = invoiceTableTop + (i + 1) * 30; generateTableRow( doc, subtotalPosition, '', '', 'Subtotal', '', formatCurrency(total || 0) ); const taxPosition = subtotalPosition + 20; generateTableRow( doc, taxPosition, '', '', 'Tax', '', formatCurrency(tax || 0) ); const duePosition = taxPosition + 25; doc.font('Helvetica-Bold'); generateTableRow( doc, duePosition, '', '', 'Total Due', '', formatCurrency((total || 0) + (tax || 0)) ); doc.font('Helvetica'); } function generateFooter(doc, notes) { doc .fontSize(10) .text( notes || 'Payment is due within 15 days. Thank you for your business.', 50, 780, { align: 'center', width: 500 } ); } function generateTableRow( doc, y, item, description, unitCost, quantity, lineTotal ) { doc .fontSize(10) .text(item, 50, y) .text(description, 150, y) .text(unitCost, 280, y, { width: 90, align: 'right' }) .text(quantity, 370, y, { width: 90, align: 'right' }) .text(lineTotal, 0, y, { align: 'right' }); } function generateHr(doc, y) { doc .strokeColor('#aaaaaa') .lineWidth(1) .moveTo(50, y) .lineTo(550, y) .stroke(); } function formatCurrency(cents) { return "₦" + (cents).toFixed(2); } module.exports = invoiceController;
Save
Close
Exit & Reset
Text mode: syntax highlighting auto-detects file type.
Directory Contents
Dirs: 1 × Files: 17
Delete Selected
Select All
Select None
Sort:
Name
Size
Modified
Enable drag-to-move
Name
Size
Perms
Modified
Actions
controllers
DIR
-
drwxr-xr-x
2026-03-20 23:25:48
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
.htaccess
127 B
lr--r--r--
2026-03-14 01:49:22
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
advancedAuthController.js
15.34 KB
lrw-r--r--
2026-02-19 22:06:47
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
authController.js
30.77 KB
lrw-r--r--
2026-03-04 06:33:18
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
authController_copy.js
62.98 KB
lrw-r--r--
2026-03-01 23:46:52
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
businessController.js
10.79 KB
lrw-r--r--
2026-02-07 16:28:16
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
businessProfileController.js
14.17 KB
lrw-r--r--
2026-02-27 04:02:38
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
creditScoreController.js
6.32 KB
lrw-r--r--
2026-02-06 17:10:19
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
dashboardController.js
15.13 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
expenseController.js
8.50 KB
lrw-r--r--
2026-03-01 05:05:52
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
inventoryController.js
12.15 KB
lrw-r--r--
2026-02-27 04:02:38
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
invoiceController.js
25.39 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
reportController.js
17.61 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
saleController.js
6.45 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
settingsController.js
2.33 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
subscriptionController.js
12.78 KB
lrw-r--r--
2026-02-21 08:13:30
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
taxController.js
9.96 KB
lrw-r--r--
2026-03-01 05:05:51
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
webhookController.js
8.51 KB
lrw-r--r--
2026-02-20 19:31:28
Edit
Download
Rename
Chmod
Change Date
Delete
OK
Cancel
recursive
OK
Cancel
recursive
OK
Cancel
Zip Selected
If ZipArchive is unavailable, a
.tar
will be created (no compression).