puffin-app/server/utils/emailService.js
Matt 039ddc0fa8
All checks were successful
Build and Push Docker Images / docker (push) Successful in 2m18s
Fix admin email to display correct payment amount
- 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
2025-11-03 13:47:03 +01:00

196 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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');
}