first commit

This commit is contained in:
Alaguraj0361 2026-05-18 22:14:41 +05:30
commit 1b2ee461a6
8 changed files with 1393 additions and 0 deletions

13
.env.example Normal file
View File

@ -0,0 +1,13 @@
PORT=3000
# Shopify Settings
SHOPIFY_WEBHOOK_SECRET=your_shopify_webhook_secret
SHOP_NAME="My Awesome Store"
# SMTP / Email Settings
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_email_password_or_app_password
SMTP_FROM_EMAIL=your_email@gmail.com

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
.env
webhook.log
test-email.js

53
README.md Normal file
View File

@ -0,0 +1,53 @@
# Shopify Invoice Email Plugin
This is a custom Node.js Express server that listens for Shopify `orders/create` webhooks. When an order is created, it dynamically generates a PDF invoice and sends an email to the customer with the PDF attached.
## Prerequisites
- Node.js (v14 or higher)
- A Shopify Store
- An Email Account (e.g., Gmail, SendGrid) to send emails via SMTP.
## Setup
1. **Install Dependencies**
Navigate to the directory and run:
```bash
npm install
```
2. **Environment Variables**
Copy `.env.example` to a new file named `.env`:
```bash
cp .env.example .env
```
Fill in your configuration in the `.env` file:
- `SHOPIFY_WEBHOOK_SECRET`: The webhook secret key from your Shopify Admin panel.
- `SMTP_*`: Your email provider's SMTP settings. (If using Gmail, use an App Password).
3. **Start the Server**
```bash
node server.js
```
## Shopify Configuration
1. Log in to your Shopify Admin.
2. Go to **Settings** > **Notifications**.
3. Scroll down to **Webhooks** and click **Create webhook**.
4. Set the **Event** to `Order creation`.
5. Set the **Format** to `JSON`.
6. Set the **URL** to your server's endpoint (e.g., `https://your-domain.com/webhooks/orders/create`).
*(Note: You will need a tool like ngrok to test locally).*
7. Set the **Webhook API version** to the latest version.
8. Save the webhook.
At the bottom of the Webhooks section in Shopify, you'll see a message saying "All your webhooks will be signed with xxx...". Use that secret string as your `SHOPIFY_WEBHOOK_SECRET` in the `.env` file.
## Testing Locally
If you are developing locally, you can use `ngrok` to expose your local port 3000 to the internet:
```bash
ngrok http 3000
```
Use the `https` ngrok URL in your Shopify webhook configuration.

50
mailer.js Normal file
View File

@ -0,0 +1,50 @@
const nodemailer = require('nodemailer');
/**
* Sends an email with the PDF attachment.
* @param {string} toEmail - The recipient's email address
* @param {string|number} orderNumber - The Shopify order number
* @param {Buffer} pdfBuffer - The PDF data buffer
*/
const sendEmailWithAttachment = async (toEmail, orderNumber, pdfBuffer) => {
// Create a transporter using your email service credentials
// You can configure SMTP, SendGrid, Amazon SES, etc.
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT) || 465,
secure: process.env.SMTP_SECURE === 'true', // true for 465, false for other ports
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
tls: {
rejectUnauthorized: false
}
});
const mailOptions = {
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: `<p>Thank you for your order!</p><p>Attached is the invoice for order <b>#${orderNumber}</b>.</p>`,
attachments: [
{
filename: `Invoice_${orderNumber}.pdf`,
content: pdfBuffer,
contentType: 'application/pdf'
}
]
};
try {
const info = await transporter.sendMail(mailOptions);
console.log(`Email sent: ${info.messageId}`);
return info;
} catch (error) {
console.error('Error sending email:', error);
throw error;
}
};
module.exports = { sendEmailWithAttachment };

1082
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "invoice-email-plugin",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"crypto": "^1.0.1",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"nodemailer": "^8.0.7",
"pdfkit": "^0.18.0"
}
}

90
pdfGenerator.js Normal file
View File

@ -0,0 +1,90 @@
const PDFDocument = require('pdfkit');
/**
* Generates a PDF invoice based on Shopify order data.
* @param {Object} orderData - The Shopify order payload
* @returns {Promise<Buffer>} - A promise that resolves to a Buffer containing the PDF data
*/
const generateInvoicePDF = (orderData) => {
return new Promise((resolve, reject) => {
try {
const doc = new PDFDocument({ margin: 50 });
let buffers = [];
doc.on('data', buffers.push.bind(buffers));
doc.on('end', () => {
const pdfData = Buffer.concat(buffers);
resolve(pdfData);
});
// Invoice Header
doc.fontSize(20).text('Invoice', { align: 'right' });
doc.moveDown();
doc.fontSize(12)
.text(`Order Number: ${orderData.name || orderData.order_number}`)
.text(`Date: ${new Date(orderData.created_at).toLocaleDateString()}`)
.moveDown();
// 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 || ''}`);
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' });
doc.moveTo(50, doc.y).lineTo(550, doc.y).stroke();
doc.moveDown(0.5);
// Line Items Table Rows
doc.font('Helvetica');
let currentY = doc.y;
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' });
doc.moveDown(0.5);
currentY = doc.y;
});
}
doc.moveTo(50, currentY).lineTo(550, currentY).stroke();
doc.moveDown(1);
// 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.end();
} catch (error) {
reject(error);
}
});
};
module.exports = { generateInvoicePDF };

82
server.js Normal file
View File

@ -0,0 +1,82 @@
require('dotenv').config({ override: true });
const express = require('express');
const crypto = require('crypto');
const { generateInvoicePDF } = require('./pdfGenerator');
const { sendEmailWithAttachment } = require('./mailer');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to capture raw body for Shopify webhook HMAC verification
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
// Function to verify Shopify Webhook
const verifyShopifyWebhook = (req, res, next) => {
const hmacHeader = req.get('X-Shopify-Hmac-Sha256');
const body = req.rawBody;
const secret = process.env.SHOPIFY_WEBHOOK_SECRET;
if (!hmacHeader || !body || !secret) {
return res.status(401).send('Webhook verification failed: Missing headers or secret.');
}
const generatedHash = crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('base64');
if (generatedHash !== hmacHeader) {
return res.status(401).send('Webhook verification failed: Invalid HMAC.');
}
next();
};
// Webhook endpoint for Order Creation
app.post('/webhooks/orders/create', verifyShopifyWebhook, async (req, res) => {
const fs = require('fs');
const logMessage = `[${new Date().toISOString()}] Received webhook request\n`;
fs.appendFileSync('webhook.log', logMessage);
// Acknowledge receipt of the webhook immediately
res.status(200).send('Webhook received');
try {
const orderData = req.body;
const processMessage = `[${new Date().toISOString()}] Processing Order ${orderData.order_number}\n`;
fs.appendFileSync('webhook.log', processMessage);
// 1. Generate the PDF invoice
const pdfBuffer = await generateInvoicePDF(orderData);
// 2. Send the email with the attached PDF
const customerEmail = orderData.email || orderData.contact_email;
if (!customerEmail) {
const errorMsg = `[${new Date().toISOString()}] Order ${orderData.order_number} has no email address associated.\n`;
fs.appendFileSync('webhook.log', errorMsg);
return;
}
await sendEmailWithAttachment(
customerEmail,
orderData.order_number,
pdfBuffer
);
const successMsg = `[${new Date().toISOString()}] Successfully processed and sent email for Order ${orderData.order_number}\n`;
fs.appendFileSync('webhook.log', successMsg);
} catch (error) {
const errorMsg = `[${new Date().toISOString()}] Error processing order webhook: ${error.stack}\n`;
fs.appendFileSync('webhook.log', errorMsg);
console.error('Error processing order webhook:', error);
}
});
app.listen(PORT, () => {
console.log(`Shopify Invoice Email Plugin running on port ${PORT}`);
});