199 lines
7.9 KiB
JavaScript

require('dotenv').config({ override: true });
const express = require('express');
const cors = require('cors');
const crypto = require('crypto');
const { generateInvoicePDF } = require('./pdfGenerator');
const { sendEmailWithAttachment } = require('./mailer');
const app = express();
app.use(cors()); // Allow frontend Shopify store to call this server
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);
// Fetch product images from Shopify Admin API (requires read_products scope)
if (orderData.line_items && orderData.line_items.length > 0) {
await Promise.all(orderData.line_items.map(async (item) => {
if (!item.product_id) return;
try {
const shopifyUrl = `https://${process.env.SHOPIFY_STORE_DOMAIN}/admin/api/2024-01/products/${item.product_id}.json`;
const response = await fetch(shopifyUrl, {
headers: {
'X-Shopify-Access-Token': process.env.SHOPIFY_ADMIN_API_TOKEN
}
});
if (response.ok) {
const data = await response.json();
const product = data.product;
if (product) {
// Find variant-specific image
let imgUrl = null;
if (item.variant_id && product.variants) {
const variant = product.variants.find(v => v.id === item.variant_id);
if (variant && variant.image_id && product.images) {
const variantImg = product.images.find(img => img.id === variant.image_id);
if (variantImg) imgUrl = variantImg.src;
}
}
// Fallback to main product image
item.image_url = imgUrl || product.image?.src || (product.images && product.images[0]?.src) || null;
}
} else {
const errText = await response.text();
console.error(`Shopify API error fetching product ${item.product_id}: Status ${response.status} - ${errText}`);
}
} catch (err) {
console.error(`Failed to fetch image for product ${item.product_id}:`, err);
}
}));
}
// 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,
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}`);
});
// Endpoint to create order from custom checkout frontend
app.post('/create-order', async (req, res) => {
try {
const { customerData, cartItems } = req.body;
const line_items = cartItems.map(item => ({
variant_id: item.variant_id || item.id,
quantity: item.quantity,
price: (item.price / 100).toFixed(2)
}));
const orderPayload = {
order: {
line_items: line_items,
customer: {
first_name: customerData.first_name,
last_name: customerData.last_name,
email: customerData.email,
phone: customerData.phone
},
billing_address: {
first_name: customerData.first_name,
last_name: customerData.last_name,
address1: customerData.address1,
address2: customerData.address2,
city: customerData.city,
province: customerData.province,
zip: customerData.zip,
country: "India",
phone: customerData.phone
},
shipping_address: {
first_name: customerData.first_name,
last_name: customerData.last_name,
address1: customerData.address1,
address2: customerData.address2,
city: customerData.city,
province: customerData.province,
zip: customerData.zip,
country: "India",
phone: customerData.phone
},
email: customerData.email,
tags: "custom_checkout",
financial_status: "pending", // Payment to be done on WhatsApp
shipping_lines: [
{
title: customerData.courier || "Standard Courier",
price: "0.00" // Update this if shipping has logic
}
]
}
};
const shopifyResponse = await fetch(`https://${process.env.SHOPIFY_STORE_DOMAIN}/admin/api/2024-01/orders.json`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': process.env.SHOPIFY_ADMIN_API_TOKEN
},
body: JSON.stringify(orderPayload)
});
const data = await shopifyResponse.json();
if (!shopifyResponse.ok) {
console.error("Shopify Order Creation Failed:", JSON.stringify(data, null, 2));
return res.status(400).json({ error: 'Failed to create order', details: data });
}
res.status(200).json({ success: true, order: data.order });
} catch (error) {
console.error("Error creating order:", error);
res.status(500).json({ error: 'Internal Server Error' });
}
});