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) => ` +
| + + |
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.
-
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.
+