first commit
This commit is contained in:
commit
1b2ee461a6
13
.env.example
Normal file
13
.env.example
Normal 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
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
.env
|
||||
webhook.log
|
||||
test-email.js
|
||||
53
README.md
Normal file
53
README.md
Normal 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
50
mailer.js
Normal 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
1082
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal 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
90
pdfGenerator.js
Normal 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
82
server.js
Normal 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}`);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user