diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..388d42c Binary files /dev/null and b/logo.png differ diff --git a/pdfGenerator.js b/pdfGenerator.js index 696b975..3974d84 100644 --- a/pdfGenerator.js +++ b/pdfGenerator.js @@ -1,14 +1,51 @@ const PDFDocument = require('pdfkit'); /** - * Generates a PDF invoice based on Shopify order data. + * Helper to draw a mock barcode in the top right corner + */ +function drawMockBarcode(doc, x, y, width, height) { + doc.save(); + let currentX = x; + const endX = x + width; + + // Draw a series of random thin/thick black lines to look like a barcode + while (currentX < endX) { + const lineWidth = Math.random() * 2 + 0.8; // 0.8 to 2.8 pixels wide + const spacing = Math.random() * 3 + 1.2; // 1.2 to 4.2 pixels spacing + + if (currentX + lineWidth > endX) break; + + doc.rect(currentX, y, lineWidth, height).fill('#000000'); + currentX += lineWidth + spacing; + } + doc.restore(); +} + +/** + * Fetch image buffer from URL safely + */ +async function fetchImageBuffer(url) { + if (!url) return null; + try { + const response = await fetch(url); + if (!response.ok) return null; + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(arrayBuffer); + } catch (err) { + console.error(`Failed to fetch image from ${url}:`, err.message); + return null; + } +} + +/** + * Generates a PDF invoice matching the user's custom layout. * @param {Object} orderData - The Shopify order payload * @returns {Promise} - A promise that resolves to a Buffer containing the PDF data */ -const generateInvoicePDF = (orderData) => { - return new Promise((resolve, reject) => { +const generateInvoicePDF = async (orderData) => { + return new Promise(async (resolve, reject) => { try { - const doc = new PDFDocument({ margin: 50 }); + const doc = new PDFDocument({ margin: 40, size: 'A4' }); let buffers = []; doc.on('data', buffers.push.bind(buffers)); @@ -17,68 +54,268 @@ const generateInvoicePDF = (orderData) => { resolve(pdfData); }); - // Invoice Header - doc.fontSize(20).text('Invoice', { align: 'right' }); - doc.moveDown(); + // Colors + const primaryColor = '#1e293b'; // Slate dark gray + const textColor = '#334155'; + const lightGray = '#cbd5e1'; + const rowAltColor = '#f8fafc'; - doc.fontSize(12) - .text(`Order Number: ${orderData.name || orderData.order_number}`) - .text(`Date: ${new Date(orderData.created_at).toLocaleDateString()}`) - .moveDown(); + // --- 1. TITLE & BARCODE --- + // Title (Left) + const isQuote = orderData.gateway === 'Request Quote' || orderData.payment_gateway_names?.includes('Request Quote'); + const titleText = isQuote ? 'Request Quote' : 'Invoice'; + + doc.fontSize(28) + .font('Helvetica-Bold') + .fillColor(primaryColor) + .text(titleText, 40, 40); - // Store Info (You can customize this) - const shopName = process.env.SHOP_NAME || 'Your Store Name'; - doc.fontSize(14).text(shopName); - doc.moveDown(); - - // Customer Info - doc.fontSize(14).text('Billed To:'); - doc.fontSize(12).text(`${orderData.billing_address?.first_name || ''} ${orderData.billing_address?.last_name || ''}`); - doc.text(`${orderData.billing_address?.address1 || ''}`); - if (orderData.billing_address?.address2) { - doc.text(`${orderData.billing_address.address2}`); - } - doc.text(`${orderData.billing_address?.city || ''}, ${orderData.billing_address?.province || ''} ${orderData.billing_address?.zip || ''}`); - doc.text(`${orderData.billing_address?.country || ''}`); + // Barcode (Right) + drawMockBarcode(doc, 380, 40, 175, 40); doc.moveDown(2); - // Line Items Table Header - doc.fontSize(12).font('Helvetica-Bold'); - doc.text('Item', 50, doc.y, { continued: true }); - doc.text('Qty', 300, doc.y, { continued: true, width: 50, align: 'right' }); - doc.text('Price', 350, doc.y, { continued: true, width: 100, align: 'right' }); - doc.text('Total', 450, doc.y, { width: 100, align: 'right' }); + // --- 2. LOGO & FROM DETAILS --- + const fromY = 110; - doc.moveTo(50, doc.y).lineTo(550, doc.y).stroke(); - doc.moveDown(0.5); + // Logo (Left) + let logoLoaded = false; + const fs = require('fs'); + const path = require('path'); + const localLogoPath = path.join(__dirname, 'logo.png'); - // Line Items Table Rows - doc.font('Helvetica'); - let currentY = doc.y; + if (fs.existsSync(localLogoPath)) { + try { + doc.image(localLogoPath, 40, fromY, { width: 130 }); + logoLoaded = true; + } catch (e) { + console.error('Error rendering local logo image:', e); + } + } + + if (!logoLoaded) { + const logoUrl = process.env.SHOP_LOGO_URL; + const logoBuffer = await fetchImageBuffer(logoUrl); + if (logoBuffer) { + try { + doc.image(logoBuffer, 40, fromY, { width: 130 }); + logoLoaded = true; + } catch (e) { + console.error('Error rendering logo image from URL:', e); + } + } + } + + if (!logoLoaded) { + // Stylish text fallback if image is missing + doc.fontSize(20) + .font('Helvetica-Bold') + .fillColor('#db2777') // Pink accent + .text('RAY AARI SHOP', 40, fromY); + } + + // From Address (Right) + doc.fontSize(10) + .font('Helvetica-Bold') + .fillColor(primaryColor) + .text('From', 380, fromY) + .font('Helvetica') + .fillColor(textColor) + .text('Ray Aari Shop', 380, fromY + 15) + .text('Sivanatham Salai', 380, fromY + 28) + .text('Arappalayam Cross Rd, Near : Madura Coats pvt', 380, fromY + 41, { width: 175 }) + .text('Madurai 625016', 380, fromY + 67) + .text('Tamil Nadu, India', 380, fromY + 80) + .text('9994333548', 380, fromY + 93); + + // --- 3. BILL TO & ORDER DETAILS --- + const detailsY = 240; + + // Bill To (Left) + const billing = orderData.billing_address || {}; + doc.fontSize(11) + .font('Helvetica-Bold') + .fillColor(primaryColor) + .text('Bill to', 40, detailsY); + + doc.fontSize(10) + .font('Helvetica') + .fillColor(textColor); + let billTextY = detailsY + 18; + if (billing.first_name || billing.last_name) { + doc.text(`${billing.first_name || ''} ${billing.last_name || ''}`, 40, billTextY); + billTextY += 13; + } + if (billing.address1) { + doc.text(billing.address1, 40, billTextY); + billTextY += 13; + } + if (billing.address2) { + doc.text(billing.address2, 40, billTextY); + billTextY += 13; + } + if (billing.city || billing.province || billing.zip) { + doc.text(`${billing.city || ''} ${billing.province || ''} ${billing.zip || ''}`, 40, billTextY); + billTextY += 13; + } + if (billing.country) { + doc.text(billing.country, 40, billTextY); + billTextY += 13; + } + if (orderData.email || orderData.contact_email) { + doc.text(orderData.email || orderData.contact_email, 40, billTextY); + billTextY += 13; + } + if (billing.phone || orderData.phone) { + doc.text(billing.phone || orderData.phone, 40, billTextY); + } + + // Order Details (Right) + const orderDateStr = new Date(orderData.created_at).toLocaleDateString('en-GB'); // DD-MM-YYYY + const gatewayName = orderData.gateway || 'Request Quote'; + + doc.fontSize(14) + .font('Helvetica-Bold') + .fillColor(primaryColor) + .text(`Order no: ${orderData.order_number}`, 380, detailsY) + .fontSize(10) + .font('Helvetica-Bold') + .text('Order date: ', 380, detailsY + 22, { continued: true }) + .font('Helvetica') + .fillColor(textColor) + .text(orderDateStr) + .font('Helvetica-Bold') + .fillColor(primaryColor) + .text('Payment method: ', 380, detailsY + 37, { continued: true }) + .font('Helvetica') + .fillColor(textColor) + .text(gatewayName); + + // --- 4. PRODUCT TABLE --- + let tableY = 380; + + // Table Headers + doc.fontSize(9) + .font('Helvetica-Bold') + .fillColor(primaryColor); + + doc.text('S.No', 40, tableY, { width: 35 }); + doc.text('Image', 85, tableY, { width: 60 }); + doc.text('Product', 155, tableY, { width: 200 }); + doc.text('Quantity', 365, tableY, { width: 50, align: 'right' }); + doc.text('Unit price', 425, tableY, { width: 60, align: 'right' }); + doc.text('Total price', 495, tableY, { width: 60, align: 'right' }); + + // Line below header + doc.moveTo(40, tableY + 12) + .lineTo(555, tableY + 12) + .lineWidth(0.8) + .strokeColor(lightGray) + .stroke(); + + tableY += 20; + + // Currency formatting symbol + const currencySymbol = orderData.currency === 'INR' ? 'Rs. ' : `${orderData.currency} `; + + // Table Rows if (orderData.line_items && orderData.line_items.length > 0) { - orderData.line_items.forEach(item => { - doc.text(item.name, 50, currentY, { width: 250 }); - doc.text(item.quantity.toString(), 300, currentY, { width: 50, align: 'right' }); - doc.text(`${orderData.currency} ${parseFloat(item.price).toFixed(2)}`, 350, currentY, { width: 100, align: 'right' }); - const total = parseFloat(item.price) * item.quantity; - doc.text(`${orderData.currency} ${total.toFixed(2)}`, 450, currentY, { width: 100, align: 'right' }); + for (let i = 0; i < orderData.line_items.length; i++) { + const item = orderData.line_items[i]; - doc.moveDown(0.5); - currentY = doc.y; - }); + // Fetch product image buffer + let imageBuffer = null; + if (item.image) { + imageBuffer = await fetchImageBuffer(item.image); + } else if (item.featured_image && item.featured_image.url) { + imageBuffer = await fetchImageBuffer(item.featured_image.url); + } + + // Background color for alternating rows + if (i % 2 === 1) { + doc.rect(40, tableY - 4, 515, 38).fill(rowAltColor); + } + + doc.fontSize(9) + .font('Helvetica') + .fillColor(textColor); + + // S.No + doc.text((i + 1).toString(), 40, tableY); + + // Image + if (imageBuffer) { + try { + doc.image(imageBuffer, 85, tableY - 2, { fit: [35, 30] }); + } catch (err) { + console.error('Failed to draw item image:', err); + } + } + + // Product Name and Variant/Quantity details + const productText = item.title || item.name; + doc.text(productText, 155, tableY, { width: 200, height: 15, ellipsis: true }); + + // Display quantity/variant info if available + if (item.variant_title) { + doc.fontSize(8) + .fillColor('#64748b') + .text(item.variant_title, 155, tableY + 12, { width: 200 }); + } + + // Reset font size + doc.fontSize(9).fillColor(textColor); + + // Quantity + doc.text(item.quantity.toString(), 365, tableY, { width: 50, align: 'right' }); + + // Unit Price + const unitPrice = parseFloat(item.price).toFixed(2); + doc.text(`${currencySymbol}${unitPrice}`, 425, tableY, { width: 60, align: 'right' }); + + // Total Price + const totalPrice = (parseFloat(item.price) * item.quantity).toFixed(2); + doc.text(`${currencySymbol}${totalPrice}`, 495, tableY, { width: 60, align: 'right' }); + + // Divider Line + doc.moveTo(40, tableY + 28) + .lineTo(555, tableY + 28) + .lineWidth(0.5) + .strokeColor('#f1f5f9') + .stroke(); + + tableY += 34; + } } - doc.moveTo(50, currentY).lineTo(550, currentY).stroke(); - doc.moveDown(1); + // --- 5. TOTALS SECTION --- + tableY += 10; - // Totals - doc.text(`Subtotal: ${orderData.currency} ${orderData.subtotal_price}`, { align: 'right' }); - if (orderData.total_tax > 0) { - doc.text(`Tax: ${orderData.currency} ${orderData.total_tax}`, { align: 'right' }); - } - doc.text(`Shipping: ${orderData.currency} ${orderData.total_shipping_price_set?.shop_money?.amount || '0.00'}`, { align: 'right' }); - doc.font('Helvetica-Bold').text(`Total: ${orderData.currency} ${orderData.total_price}`, { align: 'right' }); + doc.fontSize(10) + .font('Helvetica') + .fillColor(textColor); + + // Subtotal + doc.text('Subtotal', 365, tableY, { width: 100, align: 'right' }); + doc.font('Helvetica-Bold') + .text(`${currencySymbol}${parseFloat(orderData.subtotal_price).toFixed(2)}`, 480, tableY, { width: 75, align: 'right' }); + + // Divider + doc.moveTo(365, tableY + 15) + .lineTo(555, tableY + 15) + .lineWidth(0.5) + .strokeColor(lightGray) + .stroke(); + + tableY += 22; + + // Total + doc.fontSize(11) + .font('Helvetica-Bold') + .fillColor(primaryColor); + + doc.text('Total', 365, tableY, { width: 100, align: 'right' }); + doc.text(`${currencySymbol}${parseFloat(orderData.total_price).toFixed(2)}`, 480, tableY, { width: 75, align: 'right' }); doc.end(); } catch (error) {