add shipping label generation and email functionality
This commit is contained in:
parent
a6e413a593
commit
493fbcc5c0
66
mailer.js
66
mailer.js
@ -7,9 +7,10 @@ const path = require('path');
|
||||
* Sends a premium custom email with the PDF attachment.
|
||||
* @param {string} toEmail - The recipient's email address
|
||||
* @param {Object} orderData - The Shopify order payload
|
||||
* @param {Buffer} pdfBuffer - The PDF data buffer
|
||||
* @param {Buffer} pdfBuffer - The invoice PDF data buffer
|
||||
* @param {Buffer} [shippingLabelBuffer] - Optional shipping label PDF buffer
|
||||
*/
|
||||
const sendEmailWithAttachment = async (toEmail, orderData, pdfBuffer) => {
|
||||
const sendEmailWithAttachment = async (toEmail, orderData, pdfBuffer, shippingLabelBuffer = null) => {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT) || 465,
|
||||
@ -209,8 +210,8 @@ const sendEmailWithAttachment = async (toEmail, orderData, pdfBuffer) => {
|
||||
</html>
|
||||
`;
|
||||
|
||||
// Build attachments array - always include PDF, add logo as CID inline if it exists
|
||||
const attachments = [
|
||||
// --- Customer attachments: Invoice PDF only ---
|
||||
const customerAttachments = [
|
||||
{
|
||||
filename: `Invoice_${orderNumber}.pdf`,
|
||||
content: pdfBuffer,
|
||||
@ -219,26 +220,65 @@ const sendEmailWithAttachment = async (toEmail, orderData, pdfBuffer) => {
|
||||
];
|
||||
|
||||
if (logoExists) {
|
||||
attachments.push({
|
||||
customerAttachments.push({
|
||||
filename: 'logo.png',
|
||||
path: localLogoPath,
|
||||
cid: 'store_logo' // Referenced in HTML as src="cid:store_logo"
|
||||
cid: 'store_logo'
|
||||
});
|
||||
}
|
||||
|
||||
const mailOptions = {
|
||||
// --- Seller attachments: Invoice PDF + Shipping Label PDF ---
|
||||
const sellerAttachments = [
|
||||
{
|
||||
filename: `Invoice_${orderNumber}.pdf`,
|
||||
content: pdfBuffer,
|
||||
contentType: 'application/pdf'
|
||||
}
|
||||
];
|
||||
|
||||
if (shippingLabelBuffer) {
|
||||
sellerAttachments.push({
|
||||
filename: `ShippingLabel_${orderNumber}.pdf`,
|
||||
content: shippingLabelBuffer,
|
||||
contentType: 'application/pdf'
|
||||
});
|
||||
}
|
||||
|
||||
if (logoExists) {
|
||||
sellerAttachments.push({
|
||||
filename: 'logo.png',
|
||||
path: localLogoPath,
|
||||
cid: 'store_logo'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Send to customer (Invoice only)
|
||||
const customerMail = await transporter.sendMail({
|
||||
from: `"${process.env.SHOP_NAME || 'Your Store'}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
to: toEmail,
|
||||
subject: `Invoice for your order #${orderNumber}`,
|
||||
text: `Thank you for your order! Attached is the invoice for order #${orderNumber}.`,
|
||||
html: htmlTemplate,
|
||||
attachments
|
||||
};
|
||||
attachments: customerAttachments
|
||||
});
|
||||
console.log(`Customer email sent: ${customerMail.messageId}`);
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
console.log(`Email sent: ${info.messageId}`);
|
||||
return info;
|
||||
// 2. Send to seller (Invoice + Shipping Label)
|
||||
const sellerEmail = process.env.SELLER_EMAIL;
|
||||
if (sellerEmail) {
|
||||
const sellerMail = await transporter.sendMail({
|
||||
from: `"${process.env.SHOP_NAME || 'Your Store'}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
to: sellerEmail,
|
||||
subject: `[NEW ORDER] #${orderNumber} - Invoice & Shipping Label`,
|
||||
text: `New order #${orderNumber} received. Invoice and Shipping Label attached.`,
|
||||
html: htmlTemplate,
|
||||
attachments: sellerAttachments
|
||||
});
|
||||
console.log(`Seller email sent: ${sellerMail.messageId}`);
|
||||
}
|
||||
|
||||
return customerMail;
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
throw error;
|
||||
|
||||
80
send-label-test.js
Normal file
80
send-label-test.js
Normal file
@ -0,0 +1,80 @@
|
||||
require('dotenv').config({ override: true });
|
||||
const nodemailer = require('nodemailer');
|
||||
const { generateShippingLabelPDF } = require('./shippingLabelGenerator');
|
||||
|
||||
async function sendTestShippingLabelEmail() {
|
||||
try {
|
||||
console.log('Generating dummy Shipping Label...');
|
||||
const dummyOrder = {
|
||||
name: '#9999',
|
||||
order_number: 9999,
|
||||
created_at: new Date().toISOString(),
|
||||
currency: 'INR',
|
||||
subtotal_price: '660.00',
|
||||
total_tax: '0.00',
|
||||
total_price: '660.00',
|
||||
gateway: 'Request Quote',
|
||||
shipping_lines: [{ title: 'Delhivery Courier' }],
|
||||
billing_address: {
|
||||
first_name: 'Alagu', last_name: 'Raj',
|
||||
address1: 'South Street', address2: 'Near Temple',
|
||||
city: 'Tuticorin', province: 'Tamil Nadu', zip: '628304',
|
||||
country: 'India', phone: '7871207631'
|
||||
},
|
||||
shipping_address: {
|
||||
first_name: 'Alagu', last_name: 'Raj',
|
||||
address1: 'South Street', address2: 'Near Temple',
|
||||
city: 'Tuticorin', province: 'Tamil Nadu', zip: '628304',
|
||||
country: 'India', phone: '7871207631'
|
||||
},
|
||||
email: process.env.SMTP_USER,
|
||||
line_items: [
|
||||
{ name: 'Flat Bangle 4 Cut - Each Box', quantity: 1, price: '130.00' },
|
||||
{ name: 'Glue Stick pencil - 1 piece', quantity: 3, price: '35.00' },
|
||||
{ name: 'E-8000 - 50ML - 1 Piece', quantity: 2, price: '60.00' }
|
||||
]
|
||||
};
|
||||
|
||||
const shippingLabelBuffer = await generateShippingLabelPDF(dummyOrder);
|
||||
console.log('Shipping Label PDF generated successfully.');
|
||||
const fs = require('fs');
|
||||
fs.writeFileSync('shipping-label-test.pdf', shippingLabelBuffer);
|
||||
console.log('Saved shipping-label-test.pdf to disk.');
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT) || 465,
|
||||
secure: process.env.SMTP_SECURE === 'true',
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
const targetEmail = process.env.SMTP_USER;
|
||||
console.log(`Sending email with shipping label attached to ${targetEmail}...`);
|
||||
|
||||
const mailInfo = await transporter.sendMail({
|
||||
from: `"${process.env.SHOP_NAME || 'Your Store'}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
to: targetEmail,
|
||||
subject: `Test Shipping Label - Order #9999`,
|
||||
text: `Hello, this is a test email with the generated Shipping Label PDF attached.`,
|
||||
attachments: [
|
||||
{
|
||||
filename: `ShippingLabel_9999.pdf`,
|
||||
content: shippingLabelBuffer,
|
||||
contentType: 'application/pdf'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
console.log(`Success! Test email sent: ${mailInfo.messageId}`);
|
||||
} catch (error) {
|
||||
console.error('Error sending test shipping label email:', error);
|
||||
}
|
||||
}
|
||||
|
||||
sendTestShippingLabelEmail();
|
||||
13
server.js
13
server.js
@ -3,6 +3,7 @@ const express = require('express');
|
||||
const cors = require('cors');
|
||||
const crypto = require('crypto');
|
||||
const { generateInvoicePDF } = require('./pdfGenerator');
|
||||
const { generateShippingLabelPDF } = require('./shippingLabelGenerator');
|
||||
const { sendEmailWithAttachment } = require('./mailer');
|
||||
|
||||
const app = express();
|
||||
@ -105,10 +106,13 @@ app.post('/webhooks/orders/create', verifyShopifyWebhook, async (req, res) => {
|
||||
}));
|
||||
}
|
||||
|
||||
// 1. Generate the PDF invoice
|
||||
const pdfBuffer = await generateInvoicePDF(orderData);
|
||||
// 1. Generate the PDF invoice and shipping label in parallel
|
||||
const [pdfBuffer, shippingLabelBuffer] = await Promise.all([
|
||||
generateInvoicePDF(orderData),
|
||||
generateShippingLabelPDF(orderData)
|
||||
]);
|
||||
|
||||
// 2. Send the email with the attached PDF
|
||||
// 2. Send the email with the attached PDFs
|
||||
const customerEmail = orderData.email || orderData.contact_email;
|
||||
if (!customerEmail) {
|
||||
const errorMsg = `[${new Date().toISOString()}] Order ${orderData.order_number} has no email address associated.\n`;
|
||||
@ -119,7 +123,8 @@ app.post('/webhooks/orders/create', verifyShopifyWebhook, async (req, res) => {
|
||||
await sendEmailWithAttachment(
|
||||
customerEmail,
|
||||
orderData,
|
||||
pdfBuffer
|
||||
pdfBuffer,
|
||||
shippingLabelBuffer
|
||||
);
|
||||
|
||||
const successMsg = `[${new Date().toISOString()}] Successfully processed and sent email for Order ${orderData.order_number}\n`;
|
||||
|
||||
BIN
shipping-label-test.pdf
Normal file
BIN
shipping-label-test.pdf
Normal file
Binary file not shown.
249
shippingLabelGenerator.js
Normal file
249
shippingLabelGenerator.js
Normal file
@ -0,0 +1,249 @@
|
||||
const PDFDocument = require('pdfkit');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Helper to draw a clean, structured mock barcode
|
||||
*/
|
||||
function drawBarcode(doc, x, y, width, height, text) {
|
||||
doc.save();
|
||||
let currentX = x;
|
||||
const endX = x + width;
|
||||
|
||||
// Draw barcode bars
|
||||
while (currentX < endX) {
|
||||
// Vary the bar width and spacing to make it look like a real barcode
|
||||
const isWide = Math.random() > 0.6;
|
||||
const lineWidth = isWide ? 2.2 : 0.9;
|
||||
const spacing = Math.random() > 0.4 ? 1.5 : 2.5;
|
||||
|
||||
if (currentX + lineWidth > endX) break;
|
||||
|
||||
doc.rect(currentX, y, lineWidth, height - 16).fill('#000000');
|
||||
currentX += lineWidth + spacing;
|
||||
}
|
||||
|
||||
// Barcode number text below bars
|
||||
doc.fontSize(8.5)
|
||||
.font('Helvetica-Bold')
|
||||
.fillColor('#000000')
|
||||
.text(text, x, y + height - 11, { width: width, align: 'center', characterSpacing: 1.5 });
|
||||
|
||||
doc.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to adjust order number using ORDER_NUMBER_OFFSET
|
||||
*/
|
||||
const getAdjustedOrderNumber = (orderNumberOrName) => {
|
||||
if (!orderNumberOrName) return '';
|
||||
const offset = parseInt(process.env.ORDER_NUMBER_OFFSET, 10) || 0;
|
||||
const str = orderNumberOrName.toString();
|
||||
const matches = str.match(/\d+/);
|
||||
if (!matches) return str;
|
||||
const num = parseInt(matches[0], 10);
|
||||
return (num + offset).toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a professional 4x6 inch shipping label PDF.
|
||||
* @param {Object} orderData - The Shopify order payload
|
||||
* @returns {Promise<Buffer>} - A promise that resolves to a Buffer containing the PDF data
|
||||
*/
|
||||
const generateShippingLabelPDF = async (orderData) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
// 4x6 inch = 288 x 432 points (1 inch = 72 points)
|
||||
const doc = new PDFDocument({
|
||||
margin: 0,
|
||||
size: [288, 432]
|
||||
});
|
||||
|
||||
let buffers = [];
|
||||
doc.on('data', buffers.push.bind(buffers));
|
||||
doc.on('end', () => resolve(Buffer.concat(buffers)));
|
||||
|
||||
const W = 288;
|
||||
const H = 432;
|
||||
const PAD = 16;
|
||||
|
||||
// --- BACKGROUND ---
|
||||
doc.rect(0, 0, W, H).fill('#ffffff');
|
||||
|
||||
// --- OUTER BORDER ---
|
||||
doc.rect(6, 6, W - 12, H - 12)
|
||||
.lineWidth(2)
|
||||
.strokeColor('#1e293b')
|
||||
.stroke();
|
||||
|
||||
// ============================================================
|
||||
// SECTION 1: HEADER STRIP
|
||||
// ============================================================
|
||||
const headerY = 6;
|
||||
const headerH = 58;
|
||||
doc.rect(6, headerY, W - 12, headerH).fill('#1e293b');
|
||||
|
||||
// Load local logo if it exists
|
||||
const localLogoPath = path.join(__dirname, 'logo.png');
|
||||
let logoLoaded = false;
|
||||
if (fs.existsSync(localLogoPath)) {
|
||||
try {
|
||||
doc.image(localLogoPath, PAD, headerY + 9, { height: 40 });
|
||||
logoLoaded = true;
|
||||
} catch (e) { /* silent fail */ }
|
||||
}
|
||||
|
||||
// Shop name text
|
||||
doc.fontSize(12.5)
|
||||
.font('Helvetica-Bold')
|
||||
.fillColor('#ffffff')
|
||||
.text('RAY AARI SHOP', logoLoaded ? 95 : PAD, headerY + 13, { width: W - (logoLoaded ? 111 : PAD * 2) });
|
||||
|
||||
doc.fontSize(7.5)
|
||||
.font('Helvetica')
|
||||
.fillColor('#cbd5e1')
|
||||
.text('Sivanatham Salai, Arappalayam Cross Rd,', logoLoaded ? 95 : PAD, headerY + 28, { width: W - (logoLoaded ? 111 : PAD * 2) })
|
||||
.text('Madurai 625016, Tamil Nadu, India | Ph: 9994333548', logoLoaded ? 95 : PAD, headerY + 38, { width: W - (logoLoaded ? 111 : PAD * 2) });
|
||||
|
||||
// ============================================================
|
||||
// SECTION 2: ORDER INFO STRIP
|
||||
// ============================================================
|
||||
const orderStripY = headerY + headerH;
|
||||
const orderStripH = 30;
|
||||
doc.rect(6, orderStripY, W - 12, orderStripH).fill('#f1f5f9');
|
||||
|
||||
const orderNumber = getAdjustedOrderNumber(orderData.order_number || orderData.name);
|
||||
const orderDate = new Date(orderData.created_at || Date.now()).toLocaleDateString('en-GB');
|
||||
const shippingMethod = orderData.shipping_lines?.[0]?.title || 'Standard Courier';
|
||||
|
||||
// Order Number
|
||||
doc.fontSize(9.5)
|
||||
.font('Helvetica-Bold')
|
||||
.fillColor('#0f172a')
|
||||
.text(`ORDER: #${orderNumber}`, PAD, orderStripY + 10);
|
||||
|
||||
// Date | Courier Method
|
||||
doc.fontSize(8)
|
||||
.font('Helvetica-Bold')
|
||||
.fillColor('#475569')
|
||||
.text(`${orderDate} | ${shippingMethod}`, PAD, orderStripY + 11, { width: W - PAD * 2, align: 'right' });
|
||||
|
||||
// ============================================================
|
||||
// SECTION 3: SHIP TO SECTION HEADER
|
||||
// ============================================================
|
||||
const shipToLabelY = orderStripY + orderStripH;
|
||||
const shipToLabelH = 18;
|
||||
doc.rect(6, shipToLabelY, W - 12, shipToLabelH).fill('#0f172a');
|
||||
|
||||
doc.fontSize(8.5)
|
||||
.font('Helvetica-Bold')
|
||||
.fillColor('#ffffff')
|
||||
.text('SHIP TO', PAD, shipToLabelY + 5, { width: W - PAD * 2, align: 'center' });
|
||||
|
||||
// ============================================================
|
||||
// SECTION 4: CUSTOMER ADDRESS
|
||||
// ============================================================
|
||||
const addressStartY = shipToLabelY + shipToLabelH + 12;
|
||||
const shipping = orderData.shipping_address || orderData.billing_address || {};
|
||||
|
||||
const toName = `${shipping.first_name || ''} ${shipping.last_name || ''}`.trim() || 'Customer';
|
||||
const toPhone = shipping.phone || orderData.phone || orderData.billing_address?.phone || '';
|
||||
const toAddr1 = shipping.address1 || '';
|
||||
const toAddr2 = shipping.address2 || '';
|
||||
const toCity = shipping.city || '';
|
||||
const toProvince = shipping.province || '';
|
||||
const toZip = shipping.zip || '';
|
||||
const toCountry = shipping.country || 'India';
|
||||
|
||||
doc.fontSize(14)
|
||||
.font('Helvetica-Bold')
|
||||
.fillColor('#0f172a')
|
||||
.text(toName, PAD, addressStartY, { width: W - PAD * 2 });
|
||||
|
||||
let currentY = addressStartY + 18;
|
||||
|
||||
if (toPhone) {
|
||||
doc.fontSize(9.5)
|
||||
.font('Helvetica-Bold')
|
||||
.fillColor('#0f172a')
|
||||
.text(`Phone: ${toPhone}`, PAD, currentY, { width: W - PAD * 2 });
|
||||
currentY += 14;
|
||||
}
|
||||
|
||||
const addressParts = [];
|
||||
if (toAddr1) addressParts.push(toAddr1);
|
||||
if (toAddr2) addressParts.push(toAddr2);
|
||||
|
||||
if (toCity || toProvince || toZip) {
|
||||
const parts = [];
|
||||
if (toCity) parts.push(toCity);
|
||||
if (toProvince) parts.push(toProvince);
|
||||
const cityState = parts.join(', ');
|
||||
addressParts.push(cityState + (toZip ? ' - ' + toZip : ''));
|
||||
}
|
||||
addressParts.push(toCountry);
|
||||
|
||||
const addressText = addressParts.join('\n');
|
||||
|
||||
doc.fontSize(9.5)
|
||||
.font('Helvetica')
|
||||
.fillColor('#334155')
|
||||
.text(addressText, PAD, currentY, { width: W - PAD * 2, lineGap: 3.5 });
|
||||
|
||||
const addressHeight = doc.heightOfString(addressText, { width: W - PAD * 2, lineGap: 3.5 });
|
||||
currentY += addressHeight + 10;
|
||||
|
||||
const minSeparatorY = 250;
|
||||
const sepY = Math.max(currentY, minSeparatorY);
|
||||
|
||||
// ============================================================
|
||||
// SECTION 5: HORIZONTAL SEPARATOR
|
||||
// ============================================================
|
||||
doc.moveTo(PAD, sepY)
|
||||
.lineTo(W - PAD, sepY)
|
||||
.lineWidth(1)
|
||||
.strokeColor('#cbd5e1')
|
||||
.stroke();
|
||||
|
||||
// ============================================================
|
||||
// SECTION 6: FROM ADDRESS (Return address)
|
||||
// ============================================================
|
||||
const fromY = sepY + 10;
|
||||
|
||||
doc.fontSize(7.5)
|
||||
.font('Helvetica-Bold')
|
||||
.fillColor('#64748b')
|
||||
.text('FROM / SENDER:', PAD, fromY);
|
||||
|
||||
doc.fontSize(9)
|
||||
.font('Helvetica-Bold')
|
||||
.fillColor('#1e293b')
|
||||
.text('Ray Aari Shop', PAD, fromY + 12);
|
||||
|
||||
const shopAddressText = [
|
||||
'Sivanatham Salai, Arappalayam Cross Rd, Near Madura Coats pvt',
|
||||
'Madurai 625016, Tamil Nadu, India | Ph: 9994333548'
|
||||
].join('\n');
|
||||
|
||||
doc.fontSize(7.5)
|
||||
.font('Helvetica')
|
||||
.fillColor('#475569')
|
||||
.text(shopAddressText, PAD, fromY + 24, { width: W - PAD * 2, lineGap: 2 });
|
||||
|
||||
// ============================================================
|
||||
// SECTION 7: BARCODE STRIP
|
||||
// ============================================================
|
||||
const barcodeAreaY = H - 75;
|
||||
doc.rect(6, barcodeAreaY, W - 12, 1).fill('#cbd5e1');
|
||||
|
||||
const barcodeText = `RA${orderNumber}IN`;
|
||||
drawBarcode(doc, PAD + 15, barcodeAreaY + 12, W - PAD * 2 - 30, 52, barcodeText);
|
||||
|
||||
doc.end();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { generateShippingLabelPDF };
|
||||
Loading…
x
Reference in New Issue
Block a user