All checks were successful
Build and Push Docker Images / docker (push) Successful in 2m18s
- Admin notification was showing 6 instead of 600 - Root cause: double cents-to-dollars conversion - webhooks.js already converts to dollars before calling sendAdminNotification - Updated sendAdminNotification to handle pre-converted dollar amounts - Simplified formatting logic for clarity server/utils/emailService.js:155-162
196 lines
5.9 KiB
JavaScript
196 lines
5.9 KiB
JavaScript
import nodemailer from 'nodemailer';
|
||
import handlebars from 'handlebars';
|
||
import fs from 'fs/promises';
|
||
import path from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
// Helper function to format currency with commas
|
||
function formatCurrency(amount) {
|
||
const num = typeof amount === 'number' ? (amount / 100).toFixed(2) : parseFloat(amount).toFixed(2);
|
||
return num.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||
}
|
||
|
||
// Create transporter with connection pooling
|
||
const transporter = nodemailer.createTransport({
|
||
host: process.env.SMTP_HOST || 'mail.puffinoffset.com',
|
||
port: parseInt(process.env.SMTP_PORT) || 587,
|
||
secure: process.env.SMTP_SECURE === 'true', // true for 465, false for other ports
|
||
auth: {
|
||
user: process.env.SMTP_USER || 'noreply@puffinoffset.com',
|
||
pass: process.env.SMTP_PASSWORD
|
||
},
|
||
pool: true,
|
||
maxConnections: 5,
|
||
maxMessages: 100,
|
||
rateDelta: 1000,
|
||
rateLimit: 10
|
||
});
|
||
|
||
// Verify connection configuration on startup
|
||
export async function verifyConnection() {
|
||
try {
|
||
await transporter.verify();
|
||
console.log('✅ SMTP server is ready to send emails');
|
||
return true;
|
||
} catch (error) {
|
||
console.error('❌ SMTP connection failed:', error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Load and compile template
|
||
async function loadTemplate(templateName) {
|
||
try {
|
||
const templatePath = path.join(__dirname, '..', 'templates', `${templateName}.hbs`);
|
||
const templateSource = await fs.readFile(templatePath, 'utf-8');
|
||
return handlebars.compile(templateSource);
|
||
} catch (error) {
|
||
console.error(`Failed to load template ${templateName}:`, error.message);
|
||
throw new Error(`Template ${templateName} not found`);
|
||
}
|
||
}
|
||
|
||
// Send email using template
|
||
export async function sendTemplateEmail(to, subject, templateName, data) {
|
||
try {
|
||
// Load and compile template
|
||
const template = await loadTemplate(templateName);
|
||
const html = template(data);
|
||
|
||
// Create plain text version (strip HTML tags)
|
||
const text = html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
|
||
|
||
// Send email
|
||
const info = await transporter.sendMail({
|
||
from: `"${process.env.SMTP_FROM_NAME || 'Puffin Offset'}" <${process.env.SMTP_FROM_EMAIL || 'noreply@puffinoffset.com'}>`,
|
||
to,
|
||
subject,
|
||
text,
|
||
html
|
||
});
|
||
|
||
console.log(`✉️ Email sent: ${info.messageId} to ${to}`);
|
||
return {
|
||
success: true,
|
||
messageId: info.messageId,
|
||
accepted: info.accepted,
|
||
rejected: info.rejected
|
||
};
|
||
} catch (error) {
|
||
console.error('❌ Failed to send email:', error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Send receipt email
|
||
export async function sendReceiptEmail(customerEmail, orderDetails) {
|
||
const subject = `Order Confirmation - ${orderDetails.tons} tons CO₂ Offset`;
|
||
|
||
// Prepare template data
|
||
const templateData = {
|
||
tons: orderDetails.tons,
|
||
portfolioId: orderDetails.portfolioId,
|
||
baseAmount: formatCurrency(orderDetails.baseAmount),
|
||
processingFee: formatCurrency(orderDetails.processingFee),
|
||
totalAmount: formatCurrency(orderDetails.totalAmount),
|
||
orderId: orderDetails.orderId,
|
||
stripeSessionId: orderDetails.stripeSessionId,
|
||
date: new Date().toLocaleDateString('en-US', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric'
|
||
})
|
||
};
|
||
|
||
// Add portfolio projects if available
|
||
if (orderDetails.projects && orderDetails.projects.length > 0) {
|
||
templateData.projects = orderDetails.projects;
|
||
}
|
||
|
||
// Add carbon comparisons if available
|
||
if (orderDetails.comparisons && orderDetails.comparisons.length > 0) {
|
||
templateData.comparisons = orderDetails.comparisons;
|
||
}
|
||
|
||
return await sendTemplateEmail(
|
||
customerEmail,
|
||
subject,
|
||
'receipt',
|
||
templateData
|
||
);
|
||
}
|
||
|
||
// Send contact form email
|
||
export async function sendContactEmail(contactData) {
|
||
const subject = `Contact Form: ${contactData.name}`;
|
||
const adminEmail = process.env.ADMIN_EMAIL || 'matt@puffinoffset.com';
|
||
|
||
return await sendTemplateEmail(
|
||
adminEmail,
|
||
subject,
|
||
'contact',
|
||
{
|
||
name: contactData.name,
|
||
email: contactData.email,
|
||
phone: contactData.phone || 'Not provided',
|
||
company: contactData.company || 'Not provided',
|
||
message: contactData.message,
|
||
timestamp: new Date().toLocaleString('en-US', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})
|
||
}
|
||
);
|
||
}
|
||
|
||
// Send admin notification for new order
|
||
export async function sendAdminNotification(orderDetails, customerEmail) {
|
||
// totalAmount is already in dollars (converted before calling this function)
|
||
const totalAmount = typeof orderDetails.totalAmount === 'string'
|
||
? orderDetails.totalAmount
|
||
: (orderDetails.totalAmount / 100).toFixed(2);
|
||
|
||
// Format amount with commas for subject line
|
||
const formattedAmount = parseFloat(totalAmount).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||
const subject = `New Order: ${orderDetails.tons} tons CO₂ - $${formattedAmount}`;
|
||
const adminEmail = process.env.ADMIN_EMAIL || 'matt@puffinoffset.com';
|
||
|
||
// Check if admin notifications are enabled
|
||
if (process.env.ADMIN_NOTIFY_ON_ORDER === 'false') {
|
||
console.log('ℹ️ Admin notifications disabled, skipping');
|
||
return { success: true, skipped: true };
|
||
}
|
||
|
||
return await sendTemplateEmail(
|
||
adminEmail,
|
||
subject,
|
||
'admin-notification',
|
||
{
|
||
tons: orderDetails.tons,
|
||
portfolioId: orderDetails.portfolioId,
|
||
totalAmount: totalAmount,
|
||
orderId: orderDetails.orderId,
|
||
customerEmail,
|
||
timestamp: new Date().toLocaleString('en-US', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})
|
||
}
|
||
);
|
||
}
|
||
|
||
// Close transporter (for graceful shutdown)
|
||
export function closeTransporter() {
|
||
transporter.close();
|
||
console.log('📪 Email transporter closed');
|
||
}
|