diff --git a/.github/screenshots/order-confirmation-email.png b/.github/screenshots/order-confirmation-email.png new file mode 100644 index 0000000..b76b988 Binary files /dev/null and b/.github/screenshots/order-confirmation-email.png differ diff --git a/BACKEND/controllers/checkout.controller.js b/BACKEND/controllers/checkout.controller.js index c16e602..b69311f 100644 --- a/BACKEND/controllers/checkout.controller.js +++ b/BACKEND/controllers/checkout.controller.js @@ -1,7 +1,9 @@ import Product from '../models/product.model.js'; import Order from '../models/order.model.js'; +import User from '../models/user.model.js'; import mongoose from 'mongoose'; import Stripe from 'stripe'; +import { sendOrderConfirmationEmail } from '../services/email.service.js'; import { processReferralOnPurchase } from '../services/referral.service.js'; let stripe; @@ -183,6 +185,22 @@ export const stripeWebhook = async (req, res) => { paymentStatus: "completed", }); + // Send confirmation email — non-blocking; failures never break fulfillment + const customerEmail = session.customer_details?.email; + if (customerEmail) { + sendOrderConfirmationEmail(customerEmail, order).catch((err) => + console.error('[Email] Order confirmation failed:', err.message) + ); + } else if (session.metadata?.userId) { + User.findById(session.metadata.userId).select('email').lean().then((u) => { + if (u?.email) { + sendOrderConfirmationEmail(u.email, order).catch((err) => + console.error('[Email] Order confirmation failed:', err.message) + ); + } + }).catch(() => {}); + } + // Trigger referral reward if (order.user) { processReferralOnPurchase(order._id).catch(err => { diff --git a/BACKEND/services/email.service.js b/BACKEND/services/email.service.js index 43c810a..c75a2f3 100644 --- a/BACKEND/services/email.service.js +++ b/BACKEND/services/email.service.js @@ -1,51 +1,143 @@ import nodemailer from 'nodemailer'; -const isConfigured = () => - process.env.EMAIL_HOST && - process.env.EMAIL_USER && - process.env.EMAIL_PASS; - -let transporter = null; - -if (isConfigured()) { - transporter = nodemailer.createTransport({ - host: process.env.EMAIL_HOST, - port: Number(process.env.EMAIL_PORT) || 587, - secure: Number(process.env.EMAIL_PORT) === 465, - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS, - }, - }); -} else { - console.warn('[Email] EMAIL_HOST / EMAIL_USER / EMAIL_PASS not set — password-reset emails will not be sent.'); +function createTransporter() { + const { EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASS } = process.env; + if (!EMAIL_HOST || !EMAIL_USER || !EMAIL_PASS) { + console.warn('[Email] EMAIL_HOST / EMAIL_USER / EMAIL_PASS not set — emails will not be sent.'); + return null; + } + return nodemailer.createTransport({ + host: EMAIL_HOST, + port: parseInt(EMAIL_PORT || '587'), + secure: parseInt(EMAIL_PORT || '587') === 465, + auth: { user: EMAIL_USER, pass: EMAIL_PASS }, + }); +} + +const transporter = createTransporter(); +const FROM = process.env.EMAIL_FROM || process.env.EMAIL_USER || 'noreply@productstore.com'; + +async function send(to, subject, html) { + if (!transporter) { + console.log(`[Email] Would send "${subject}" to ${to} (SMTP not configured)`); + return; + } + await transporter.sendMail({ from: FROM, to, subject, html }); +} + +export async function sendOrderConfirmationEmail(toEmail, order) { + const estimatedDelivery = new Date(); + estimatedDelivery.setDate(estimatedDelivery.getDate() + 5); + const deliveryStr = estimatedDelivery.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + + const itemRows = order.items + .map( + (item) => ` + + ${item.name} + ${item.quantity} + $${(item.price * item.quantity).toFixed(2)} + ` + ) + .join(''); + + const html = ` + + + + + + +
+ + + + + + + + + + + +
+

Order Confirmed!

+

Thank you for your purchase.

+
+

+ Hi there,

+ Your order has been received and is being processed. Here's a summary: +

+ + + + + + + + + + + +
Order ID${order._id}
Estimated Delivery${deliveryStr}
+ + + + + + + + + + + ${itemRows} +
ItemQtySubtotal
+ + + + + + + +
Total Charged$${order.totalAmount.toFixed(2)}
+
+

+ Questions? Reply to this email or visit our store.
+ © ${new Date().getFullYear()} Product Store +

+
+
+ +`; + + await send(toEmail, `Order Confirmation — $${order.totalAmount.toFixed(2)}`, html); } export const sendPasswordResetEmail = async (toEmail, resetUrl) => { - if (!transporter) { - console.warn(`[Email] Would have sent password-reset email to ${toEmail}: ${resetUrl}`); - return; - } - - await transporter.sendMail({ - from: process.env.EMAIL_FROM || process.env.EMAIL_USER, - to: toEmail, - subject: 'Password Reset Request – Product Store', - html: ` -
-

Reset your password

-

You requested a password reset. Click the button below to set a new password. - This link expires in 1 hour.

- - Reset Password - -

- If you did not request this, you can safely ignore this email.
- The link will expire automatically after 1 hour. -

-
- `, - }); + if (!transporter) { + console.warn(`[Email] Would have sent password-reset email to ${toEmail}: ${resetUrl}`); + return; + } + + await transporter.sendMail({ + from: FROM, + to: toEmail, + subject: 'Password Reset Request – Product Store', + html: ` +
+

Reset your password

+

You requested a password reset. Click the button below to set a new password. + This link expires in 1 hour.

+ + Reset Password + +

+ If you did not request this, you can safely ignore this email.
+ The link will expire automatically after 1 hour. +

+
+ `, + }); };