email refactor
This commit is contained in:
@@ -6,7 +6,7 @@ const {
|
|||||||
ConditionCheck,
|
ConditionCheck,
|
||||||
} = require("../models");
|
} = require("../models");
|
||||||
const { Op } = require("sequelize");
|
const { Op } = require("sequelize");
|
||||||
const emailService = require("../services/emailService");
|
const emailServices = require("../services/email");
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
|
|
||||||
const reminderSchedule = "0 * * * *"; // Run every hour
|
const reminderSchedule = "0 * * * *"; // Run every hour
|
||||||
@@ -170,7 +170,7 @@ class ConditionCheckReminderJob {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await emailService.sendConditionCheckReminder(
|
await emailServices.rentalReminder.sendConditionCheckReminder(
|
||||||
rental.owner.email,
|
rental.owner.email,
|
||||||
notificationData,
|
notificationData,
|
||||||
rental
|
rental
|
||||||
@@ -195,7 +195,7 @@ class ConditionCheckReminderJob {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await emailService.sendConditionCheckReminder(
|
await emailServices.rentalReminder.sendConditionCheckReminder(
|
||||||
rental.renter.email,
|
rental.renter.email,
|
||||||
notificationData,
|
notificationData,
|
||||||
rental
|
rental
|
||||||
@@ -220,7 +220,7 @@ class ConditionCheckReminderJob {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await emailService.sendConditionCheckReminder(
|
await emailServices.rentalReminder.sendConditionCheckReminder(
|
||||||
rental.renter.email,
|
rental.renter.email,
|
||||||
notificationData,
|
notificationData,
|
||||||
rental
|
rental
|
||||||
@@ -245,7 +245,7 @@ class ConditionCheckReminderJob {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await emailService.sendConditionCheckReminder(
|
await emailServices.rentalReminder.sendConditionCheckReminder(
|
||||||
rental.owner.email,
|
rental.owner.email,
|
||||||
notificationData,
|
notificationData,
|
||||||
rental
|
rental
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const jwt = require("jsonwebtoken");
|
|||||||
const { OAuth2Client } = require("google-auth-library");
|
const { OAuth2Client } = require("google-auth-library");
|
||||||
const { User, AlphaInvitation } = require("../models"); // Import from models/index.js to get models with associations
|
const { User, AlphaInvitation } = require("../models"); // Import from models/index.js to get models with associations
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const emailService = require("../services/emailService");
|
const emailServices = require("../services/email");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -117,7 +117,7 @@ router.post(
|
|||||||
// Send verification email (don't block registration if email fails)
|
// Send verification email (don't block registration if email fails)
|
||||||
let verificationEmailSent = false;
|
let verificationEmailSent = false;
|
||||||
try {
|
try {
|
||||||
await emailService.sendVerificationEmail(user, user.verificationToken);
|
await emailServices.auth.sendVerificationEmail(user, user.verificationToken);
|
||||||
verificationEmailSent = true;
|
verificationEmailSent = true;
|
||||||
} catch (emailError) {
|
} catch (emailError) {
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
@@ -558,7 +558,7 @@ router.post(
|
|||||||
|
|
||||||
// Send verification email
|
// Send verification email
|
||||||
try {
|
try {
|
||||||
await emailService.sendVerificationEmail(user, user.verificationToken);
|
await emailServices.auth.sendVerificationEmail(user, user.verificationToken);
|
||||||
} catch (emailError) {
|
} catch (emailError) {
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
reqLogger.error("Failed to resend verification email", {
|
reqLogger.error("Failed to resend verification email", {
|
||||||
@@ -726,7 +726,7 @@ router.post(
|
|||||||
|
|
||||||
// Send password reset email
|
// Send password reset email
|
||||||
try {
|
try {
|
||||||
await emailService.sendPasswordResetEmail(user, resetToken);
|
await emailServices.auth.sendPasswordResetEmail(user, resetToken);
|
||||||
|
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
reqLogger.info("Password reset email sent", {
|
reqLogger.info("Password reset email sent", {
|
||||||
@@ -868,7 +868,7 @@ router.post(
|
|||||||
|
|
||||||
// Send password changed notification email
|
// Send password changed notification email
|
||||||
try {
|
try {
|
||||||
await emailService.sendPasswordChangedEmail(user);
|
await emailServices.auth.sendPasswordChangedEmail(user);
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
reqLogger.info("Password changed notification sent", {
|
reqLogger.info("Password changed notification sent", {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const { Feedback, User } = require('../models');
|
|||||||
const { authenticateToken } = require('../middleware/auth');
|
const { authenticateToken } = require('../middleware/auth');
|
||||||
const { validateFeedback, sanitizeInput } = require('../middleware/validation');
|
const { validateFeedback, sanitizeInput } = require('../middleware/validation');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const emailService = require('../services/emailService');
|
const emailServices = require('../services/email');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Submit new feedback
|
// Submit new feedback
|
||||||
@@ -29,7 +29,7 @@ router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req,
|
|||||||
|
|
||||||
// Send confirmation email to user
|
// Send confirmation email to user
|
||||||
try {
|
try {
|
||||||
await emailService.sendFeedbackConfirmation(req.user, feedback);
|
await emailServices.feedback.sendFeedbackConfirmation(req.user, feedback);
|
||||||
} catch (emailError) {
|
} catch (emailError) {
|
||||||
reqLogger.error("Failed to send feedback confirmation email", {
|
reqLogger.error("Failed to send feedback confirmation email", {
|
||||||
error: emailError.message,
|
error: emailError.message,
|
||||||
@@ -41,7 +41,7 @@ router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req,
|
|||||||
|
|
||||||
// Send notification email to admin
|
// Send notification email to admin
|
||||||
try {
|
try {
|
||||||
await emailService.sendFeedbackNotificationToAdmin(req.user, feedback);
|
await emailServices.feedback.sendFeedbackNotificationToAdmin(req.user, feedback);
|
||||||
} catch (emailError) {
|
} catch (emailError) {
|
||||||
reqLogger.error("Failed to send feedback notification to admin", {
|
reqLogger.error("Failed to send feedback notification to admin", {
|
||||||
error: emailError.message,
|
error: emailError.message,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const { ForumPost, ForumComment, PostTag, User } = require('../models');
|
|||||||
const { authenticateToken } = require('../middleware/auth');
|
const { authenticateToken } = require('../middleware/auth');
|
||||||
const { uploadForumPostImages, uploadForumCommentImages } = require('../middleware/upload');
|
const { uploadForumPostImages, uploadForumCommentImages } = require('../middleware/upload');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const emailService = require('../services/emailService');
|
const emailServices = require('../services/email');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Helper function to build nested comment tree
|
// Helper function to build nested comment tree
|
||||||
@@ -489,7 +489,7 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) =>
|
|||||||
|
|
||||||
// Only send email if not marking your own comment as answer
|
// Only send email if not marking your own comment as answer
|
||||||
if (comment && comment.authorId !== req.user.id) {
|
if (comment && comment.authorId !== req.user.id) {
|
||||||
await emailService.sendForumAnswerAcceptedNotification(
|
await emailServices.forum.sendForumAnswerAcceptedNotification(
|
||||||
comment.author,
|
comment.author,
|
||||||
postAuthor,
|
postAuthor,
|
||||||
post,
|
post,
|
||||||
@@ -617,7 +617,7 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
|
|||||||
|
|
||||||
// Send reply notification if not replying to yourself
|
// Send reply notification if not replying to yourself
|
||||||
if (parentComment && parentComment.authorId !== req.user.id) {
|
if (parentComment && parentComment.authorId !== req.user.id) {
|
||||||
await emailService.sendForumReplyNotification(
|
await emailServices.forum.sendForumReplyNotification(
|
||||||
parentComment.author,
|
parentComment.author,
|
||||||
commenter,
|
commenter,
|
||||||
postWithAuthor,
|
postWithAuthor,
|
||||||
@@ -629,7 +629,7 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
|
|||||||
} else {
|
} else {
|
||||||
// Send comment notification to post author if not commenting on your own post
|
// Send comment notification to post author if not commenting on your own post
|
||||||
if (postWithAuthor.authorId !== req.user.id) {
|
if (postWithAuthor.authorId !== req.user.id) {
|
||||||
await emailService.sendForumCommentNotification(
|
await emailServices.forum.sendForumCommentNotification(
|
||||||
postWithAuthor.author,
|
postWithAuthor.author,
|
||||||
commenter,
|
commenter,
|
||||||
postWithAuthor,
|
postWithAuthor,
|
||||||
@@ -662,7 +662,7 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
|
|||||||
// Send thread activity notifications to all unique participants
|
// Send thread activity notifications to all unique participants
|
||||||
for (const participant of participants) {
|
for (const participant of participants) {
|
||||||
if (participant.author) {
|
if (participant.author) {
|
||||||
await emailService.sendForumThreadActivityNotification(
|
await emailServices.forum.sendForumThreadActivityNotification(
|
||||||
participant.author,
|
participant.author,
|
||||||
commenter,
|
commenter,
|
||||||
postWithAuthor,
|
postWithAuthor,
|
||||||
|
|||||||
@@ -238,8 +238,8 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
|
|||||||
// If first listing, send celebration email
|
// If first listing, send celebration email
|
||||||
if (ownerItemCount === 1) {
|
if (ownerItemCount === 1) {
|
||||||
try {
|
try {
|
||||||
const emailService = require("../services/emailService");
|
const emailServices = require("../services/email");
|
||||||
await emailService.sendFirstListingCelebrationEmail(
|
await emailServices.userEngagement.sendFirstListingCelebrationEmail(
|
||||||
itemWithOwner.owner,
|
itemWithOwner.owner,
|
||||||
itemWithOwner
|
itemWithOwner
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const { uploadMessageImage } = require('../middleware/upload');
|
|||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const { emitNewMessage, emitMessageRead } = require('../sockets/messageSocket');
|
const { emitNewMessage, emitMessageRead } = require('../sockets/messageSocket');
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
const emailService = require('../services/emailService');
|
const emailServices = require('../services/email');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -293,7 +293,7 @@ router.post('/', authenticateToken, uploadMessageImage, async (req, res) => {
|
|||||||
attributes: ['id', 'firstName', 'lastName', 'email']
|
attributes: ['id', 'firstName', 'lastName', 'email']
|
||||||
});
|
});
|
||||||
|
|
||||||
await emailService.sendNewMessageNotification(receiver, sender, message);
|
await emailServices.messaging.sendNewMessageNotification(receiver, sender, message);
|
||||||
} catch (emailError) {
|
} catch (emailError) {
|
||||||
// Log email error but don't block the message send
|
// Log email error but don't block the message send
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const RentalDurationCalculator = require("../utils/rentalDurationCalculator");
|
|||||||
const RefundService = require("../services/refundService");
|
const RefundService = require("../services/refundService");
|
||||||
const LateReturnService = require("../services/lateReturnService");
|
const LateReturnService = require("../services/lateReturnService");
|
||||||
const DamageAssessmentService = require("../services/damageAssessmentService");
|
const DamageAssessmentService = require("../services/damageAssessmentService");
|
||||||
const emailService = require("../services/emailService");
|
const emailServices = require("../services/email");
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
|
|||||||
|
|
||||||
// Send rental request notification to owner
|
// Send rental request notification to owner
|
||||||
try {
|
try {
|
||||||
await emailService.sendRentalRequestEmail(rentalWithDetails);
|
await emailServices.rentalFlow.sendRentalRequestEmail(rentalWithDetails.owner, rentalWithDetails.renter, rentalWithDetails);
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
reqLogger.info("Rental request notification sent to owner", {
|
reqLogger.info("Rental request notification sent to owner", {
|
||||||
rentalId: rental.id,
|
rentalId: rental.id,
|
||||||
@@ -320,7 +320,7 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
|
|||||||
|
|
||||||
// Send rental request confirmation to renter
|
// Send rental request confirmation to renter
|
||||||
try {
|
try {
|
||||||
await emailService.sendRentalRequestConfirmationEmail(rentalWithDetails);
|
await emailServices.rentalFlow.sendRentalRequestConfirmationEmail(rentalWithDetails.renter, rentalWithDetails);
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
reqLogger.info("Rental request confirmation sent to renter", {
|
reqLogger.info("Rental request confirmation sent to renter", {
|
||||||
rentalId: rental.id,
|
rentalId: rental.id,
|
||||||
@@ -444,7 +444,7 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
|||||||
// Send confirmation emails
|
// Send confirmation emails
|
||||||
// Send approval confirmation to owner with Stripe reminder
|
// Send approval confirmation to owner with Stripe reminder
|
||||||
try {
|
try {
|
||||||
await emailService.sendRentalApprovalConfirmationEmail(updatedRental);
|
await emailServices.rentalFlow.sendRentalApprovalConfirmationEmail(updatedRental.owner, updatedRental.renter, updatedRental);
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
reqLogger.info("Rental approval confirmation sent to owner", {
|
reqLogger.info("Rental approval confirmation sent to owner", {
|
||||||
rentalId: updatedRental.id,
|
rentalId: updatedRental.id,
|
||||||
@@ -473,7 +473,7 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
|||||||
userId: updatedRental.renterId,
|
userId: updatedRental.renterId,
|
||||||
metadata: { rentalStart: updatedRental.startDateTime },
|
metadata: { rentalStart: updatedRental.startDateTime },
|
||||||
};
|
};
|
||||||
await emailService.sendRentalConfirmation(
|
await emailServices.rentalFlow.sendRentalConfirmation(
|
||||||
renter.email,
|
renter.email,
|
||||||
renterNotification,
|
renterNotification,
|
||||||
updatedRental,
|
updatedRental,
|
||||||
@@ -536,7 +536,7 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
|||||||
// Send confirmation emails
|
// Send confirmation emails
|
||||||
// Send approval confirmation to owner (for free rentals, no Stripe reminder shown)
|
// Send approval confirmation to owner (for free rentals, no Stripe reminder shown)
|
||||||
try {
|
try {
|
||||||
await emailService.sendRentalApprovalConfirmationEmail(updatedRental);
|
await emailServices.rentalFlow.sendRentalApprovalConfirmationEmail(updatedRental.owner, updatedRental.renter, updatedRental);
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
reqLogger.info("Rental approval confirmation sent to owner", {
|
reqLogger.info("Rental approval confirmation sent to owner", {
|
||||||
rentalId: updatedRental.id,
|
rentalId: updatedRental.id,
|
||||||
@@ -565,7 +565,7 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
|||||||
userId: updatedRental.renterId,
|
userId: updatedRental.renterId,
|
||||||
metadata: { rentalStart: updatedRental.startDateTime },
|
metadata: { rentalStart: updatedRental.startDateTime },
|
||||||
};
|
};
|
||||||
await emailService.sendRentalConfirmation(
|
await emailServices.rentalFlow.sendRentalConfirmation(
|
||||||
renter.email,
|
renter.email,
|
||||||
renterNotification,
|
renterNotification,
|
||||||
updatedRental,
|
updatedRental,
|
||||||
@@ -686,7 +686,7 @@ router.put("/:id/decline", authenticateToken, async (req, res) => {
|
|||||||
|
|
||||||
// Send decline notification email to renter
|
// Send decline notification email to renter
|
||||||
try {
|
try {
|
||||||
await emailService.sendRentalDeclinedEmail(updatedRental, reason);
|
await emailServices.rentalFlow.sendRentalDeclinedEmail(updatedRental.renter, updatedRental, reason);
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
reqLogger.info("Rental decline notification sent to renter", {
|
reqLogger.info("Rental decline notification sent to renter", {
|
||||||
rentalId: rental.id,
|
rentalId: rental.id,
|
||||||
@@ -1060,7 +1060,9 @@ router.post("/:id/cancel", authenticateToken, async (req, res) => {
|
|||||||
|
|
||||||
// Send cancellation notification emails
|
// Send cancellation notification emails
|
||||||
try {
|
try {
|
||||||
await emailService.sendRentalCancellationEmails(
|
await emailServices.rentalFlow.sendRentalCancellationEmails(
|
||||||
|
updatedRental.owner,
|
||||||
|
updatedRental.renter,
|
||||||
updatedRental,
|
updatedRental,
|
||||||
result.refund
|
result.refund
|
||||||
);
|
);
|
||||||
@@ -1153,7 +1155,7 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => {
|
|||||||
|
|
||||||
// Send completion emails to both renter and owner
|
// Send completion emails to both renter and owner
|
||||||
try {
|
try {
|
||||||
await emailService.sendRentalCompletionEmails(rentalWithDetails);
|
await emailServices.rentalFlow.sendRentalCompletionEmails(rentalWithDetails.owner, rentalWithDetails.renter, rentalWithDetails);
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
reqLogger.info("Rental completion emails sent", {
|
reqLogger.info("Rental completion emails sent", {
|
||||||
rentalId,
|
rentalId,
|
||||||
@@ -1221,7 +1223,9 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Send notification to customer service
|
// Send notification to customer service
|
||||||
await emailService.sendLostItemToCustomerService(updatedRental);
|
const owner = await User.findByPk(rental.ownerId);
|
||||||
|
const renter = await User.findByPk(rental.renterId);
|
||||||
|
await emailServices.customerService.sendLostItemToCustomerService(updatedRental, owner, renter);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const crypto = require("crypto");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { AlphaInvitation, User, sequelize } = require("../models");
|
const { AlphaInvitation, User, sequelize } = require("../models");
|
||||||
const emailService = require("../services/emailService");
|
const emailServices = require("../services/email");
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
|
|
||||||
// Generate unique alpha code
|
// Generate unique alpha code
|
||||||
@@ -69,7 +69,7 @@ async function addInvitation(email, notes = "") {
|
|||||||
// Send invitation email
|
// Send invitation email
|
||||||
let emailSent = false;
|
let emailSent = false;
|
||||||
try {
|
try {
|
||||||
await emailService.sendAlphaInvitation(email, code);
|
await emailServices.alphaInvitation.sendAlphaInvitation(email, code);
|
||||||
emailSent = true;
|
emailSent = true;
|
||||||
} catch (emailError) {
|
} catch (emailError) {
|
||||||
console.log(`\n⚠️ Warning: Failed to send email to ${email}`);
|
console.log(`\n⚠️ Warning: Failed to send email to ${email}`);
|
||||||
@@ -131,7 +131,7 @@ async function resendInvitation(emailOrCode) {
|
|||||||
|
|
||||||
// Resend the email
|
// Resend the email
|
||||||
try {
|
try {
|
||||||
await emailService.sendAlphaInvitation(invitation.email, invitation.code);
|
await emailServices.alphaInvitation.sendAlphaInvitation(invitation.email, invitation.code);
|
||||||
|
|
||||||
console.log(`\n✅ Alpha invitation resent successfully!`);
|
console.log(`\n✅ Alpha invitation resent successfully!`);
|
||||||
console.log(` Email: ${invitation.email}`);
|
console.log(` Email: ${invitation.email}`);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const { Rental, Item, ConditionCheck } = require("../models");
|
const { Rental, Item, ConditionCheck, User } = require("../models");
|
||||||
const LateReturnService = require("./lateReturnService");
|
const LateReturnService = require("./lateReturnService");
|
||||||
const emailService = require("./emailService");
|
const emailServices = require("./email");
|
||||||
|
|
||||||
class DamageAssessmentService {
|
class DamageAssessmentService {
|
||||||
/**
|
/**
|
||||||
@@ -119,9 +119,15 @@ class DamageAssessmentService {
|
|||||||
|
|
||||||
const updatedRental = await rental.update(updates);
|
const updatedRental = await rental.update(updates);
|
||||||
|
|
||||||
|
// Fetch owner and renter user data for email
|
||||||
|
const owner = await User.findByPk(updatedRental.ownerId);
|
||||||
|
const renter = await User.findByPk(updatedRental.renterId);
|
||||||
|
|
||||||
// Send damage report to customer service for review
|
// Send damage report to customer service for review
|
||||||
await emailService.sendDamageReportToCustomerService(
|
await emailServices.customerService.sendDamageReportToCustomerService(
|
||||||
updatedRental,
|
updatedRental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
damageAssessment,
|
damageAssessment,
|
||||||
lateCalculation
|
lateCalculation
|
||||||
);
|
);
|
||||||
|
|||||||
110
backend/services/email/core/EmailClient.js
Normal file
110
backend/services/email/core/EmailClient.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
const { SESClient, SendEmailCommand } = require("@aws-sdk/client-ses");
|
||||||
|
const { getAWSConfig } = require("../../../config/aws");
|
||||||
|
const { htmlToPlainText } = require("./emailUtils");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EmailClient handles AWS SES configuration and core email sending functionality
|
||||||
|
* This class is responsible for:
|
||||||
|
* - Initializing the AWS SES client
|
||||||
|
* - Sending emails with HTML and plain text content
|
||||||
|
* - Managing email sending state (enabled/disabled via environment)
|
||||||
|
*/
|
||||||
|
class EmailClient {
|
||||||
|
constructor() {
|
||||||
|
this.sesClient = null;
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the AWS SES client
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use centralized AWS configuration with credential profiles
|
||||||
|
const awsConfig = getAWSConfig();
|
||||||
|
this.sesClient = new SESClient(awsConfig);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
console.log("AWS SES Email Client initialized successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to initialize AWS SES Email Client:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an email using AWS SES
|
||||||
|
* @param {string|string[]} to - Email address(es) to send to
|
||||||
|
* @param {string} subject - Email subject line
|
||||||
|
* @param {string} htmlContent - HTML content of the email
|
||||||
|
* @param {string|null} textContent - Plain text content (auto-generated from HTML if not provided)
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendEmail(to, subject, htmlContent, textContent = null) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email sending is enabled in the environment
|
||||||
|
if (!process.env.EMAIL_ENABLED || process.env.EMAIL_ENABLED !== "true") {
|
||||||
|
console.log("Email sending disabled in environment");
|
||||||
|
return { success: true, messageId: "disabled" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-generate plain text from HTML if not provided
|
||||||
|
if (!textContent) {
|
||||||
|
textContent = htmlToPlainText(htmlContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use friendly sender name format for better recognition
|
||||||
|
const fromName = process.env.SES_FROM_NAME || "RentAll";
|
||||||
|
const fromEmail = process.env.SES_FROM_EMAIL;
|
||||||
|
const source = `${fromName} <${fromEmail}>`;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
Source: source,
|
||||||
|
Destination: {
|
||||||
|
ToAddresses: Array.isArray(to) ? to : [to],
|
||||||
|
},
|
||||||
|
Message: {
|
||||||
|
Subject: {
|
||||||
|
Data: subject,
|
||||||
|
Charset: "UTF-8",
|
||||||
|
},
|
||||||
|
Body: {
|
||||||
|
Html: {
|
||||||
|
Data: htmlContent,
|
||||||
|
Charset: "UTF-8",
|
||||||
|
},
|
||||||
|
Text: {
|
||||||
|
Data: textContent,
|
||||||
|
Charset: "UTF-8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add reply-to address if configured
|
||||||
|
if (process.env.SES_REPLY_TO_EMAIL) {
|
||||||
|
params.ReplyToAddresses = [process.env.SES_REPLY_TO_EMAIL];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const command = new SendEmailCommand(params);
|
||||||
|
const result = await this.sesClient.send(command);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Email sent successfully to ${to}, MessageId: ${result.MessageId}`
|
||||||
|
);
|
||||||
|
return { success: true, messageId: result.MessageId };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send email:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EmailClient;
|
||||||
443
backend/services/email/core/TemplateManager.js
Normal file
443
backend/services/email/core/TemplateManager.js
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
const fs = require("fs").promises;
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TemplateManager handles loading, caching, and rendering email templates
|
||||||
|
* This class is responsible for:
|
||||||
|
* - Loading HTML email templates from disk
|
||||||
|
* - Caching templates in memory for performance
|
||||||
|
* - Rendering templates with variable substitution
|
||||||
|
* - Providing fallback templates when files can't be loaded
|
||||||
|
*/
|
||||||
|
class TemplateManager {
|
||||||
|
constructor() {
|
||||||
|
this.templates = new Map();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the template manager by loading all email templates
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
await this.loadEmailTemplates();
|
||||||
|
this.initialized = true;
|
||||||
|
console.log("Email Template Manager initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all email templates from disk into memory
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async loadEmailTemplates() {
|
||||||
|
const templatesDir = path.join(__dirname, "..", "..", "..", "templates", "emails");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const templateFiles = [
|
||||||
|
"conditionCheckReminderToUser.html",
|
||||||
|
"rentalConfirmationToUser.html",
|
||||||
|
"emailVerificationToUser.html",
|
||||||
|
"passwordResetToUser.html",
|
||||||
|
"passwordChangedToUser.html",
|
||||||
|
"lateReturnToCS.html",
|
||||||
|
"damageReportToCS.html",
|
||||||
|
"lostItemToCS.html",
|
||||||
|
"rentalRequestToOwner.html",
|
||||||
|
"rentalRequestConfirmationToRenter.html",
|
||||||
|
"rentalCancellationConfirmationToUser.html",
|
||||||
|
"rentalCancellationNotificationToUser.html",
|
||||||
|
"rentalDeclinedToRenter.html",
|
||||||
|
"rentalApprovalConfirmationToOwner.html",
|
||||||
|
"rentalCompletionThankYouToRenter.html",
|
||||||
|
"rentalCompletionCongratsToOwner.html",
|
||||||
|
"payoutReceivedToOwner.html",
|
||||||
|
"firstListingCelebrationToOwner.html",
|
||||||
|
"alphaInvitationToUser.html",
|
||||||
|
"feedbackConfirmationToUser.html",
|
||||||
|
"feedbackNotificationToAdmin.html",
|
||||||
|
"newMessageToUser.html",
|
||||||
|
"forumCommentToPostAuthor.html",
|
||||||
|
"forumReplyToCommentAuthor.html",
|
||||||
|
"forumAnswerAcceptedToCommentAuthor.html",
|
||||||
|
"forumThreadActivityToParticipant.html",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const templateFile of templateFiles) {
|
||||||
|
try {
|
||||||
|
const templatePath = path.join(templatesDir, templateFile);
|
||||||
|
const templateContent = await fs.readFile(templatePath, "utf-8");
|
||||||
|
const templateName = path.basename(templateFile, ".html");
|
||||||
|
this.templates.set(templateName, templateContent);
|
||||||
|
console.log(`✓ Loaded template: ${templateName}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`✗ Failed to load template ${templateFile}:`,
|
||||||
|
error.message
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
` Template path: ${path.join(templatesDir, templateFile)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Loaded ${this.templates.size} of ${templateFiles.length} email templates`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load email templates:", error);
|
||||||
|
console.error("Templates directory:", templatesDir);
|
||||||
|
console.error("Error stack:", error.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a template with the provided variables
|
||||||
|
* @param {string} templateName - Name of the template to render
|
||||||
|
* @param {Object} variables - Variables to substitute in the template
|
||||||
|
* @returns {Promise<string>} Rendered HTML
|
||||||
|
*/
|
||||||
|
async renderTemplate(templateName, variables = {}) {
|
||||||
|
// Ensure service is initialized before rendering
|
||||||
|
if (!this.initialized) {
|
||||||
|
console.log(`Template manager not initialized yet, initializing now...`);
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = this.templates.get(templateName);
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
console.error(`Template not found: ${templateName}`);
|
||||||
|
console.error(
|
||||||
|
`Available templates: ${Array.from(this.templates.keys()).join(", ")}`
|
||||||
|
);
|
||||||
|
console.error(`Stack trace:`, new Error().stack);
|
||||||
|
console.log(`Using fallback template for: ${templateName}`);
|
||||||
|
template = this.getFallbackTemplate(templateName);
|
||||||
|
} else {
|
||||||
|
console.log(`✓ Template found: ${templateName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rendered = template;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object.keys(variables).forEach((key) => {
|
||||||
|
const regex = new RegExp(`{{${key}}}`, "g");
|
||||||
|
rendered = rendered.replace(regex, variables[key] || "");
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error rendering template ${templateName}:`, error);
|
||||||
|
console.error(`Stack trace:`, error.stack);
|
||||||
|
console.error(`Variables provided:`, Object.keys(variables));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a fallback template when the HTML file is not available
|
||||||
|
* @param {string} templateName - Name of the template
|
||||||
|
* @returns {string} Fallback HTML template
|
||||||
|
*/
|
||||||
|
getFallbackTemplate(templateName) {
|
||||||
|
const baseTemplate = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; background-color: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.header { text-align: center; border-bottom: 2px solid #e9ecef; padding-bottom: 20px; margin-bottom: 30px; }
|
||||||
|
.logo { font-size: 24px; font-weight: bold; color: #333; }
|
||||||
|
.content { line-height: 1.6; color: #555; }
|
||||||
|
.button { display: inline-block; background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; margin: 20px 0; }
|
||||||
|
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #e9ecef; text-align: center; font-size: 12px; color: #6c757d; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">RentAll</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
{{content}}
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>This email was sent from RentAll. If you have any questions, please contact support.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const templates = {
|
||||||
|
conditionCheckReminderToUser: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<h2>{{title}}</h2>
|
||||||
|
<p>{{message}}</p>
|
||||||
|
<p><strong>Rental Item:</strong> {{itemName}}</p>
|
||||||
|
<p><strong>Deadline:</strong> {{deadline}}</p>
|
||||||
|
<p>Please complete this condition check as soon as possible to ensure proper documentation.</p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
rentalConfirmationToUser: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{recipientName}},</p>
|
||||||
|
<h2>{{title}}</h2>
|
||||||
|
<p>{{message}}</p>
|
||||||
|
<p><strong>Item:</strong> {{itemName}}</p>
|
||||||
|
<p><strong>Rental Period:</strong> {{startDate}} to {{endDate}}</p>
|
||||||
|
<p>Thank you for using RentAll!</p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
emailVerificationToUser: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{recipientName}},</p>
|
||||||
|
<h2>Verify Your Email Address</h2>
|
||||||
|
<p>Thank you for registering with RentAll! Please verify your email address by clicking the button below.</p>
|
||||||
|
<p><a href="{{verificationUrl}}" class="button">Verify Email Address</a></p>
|
||||||
|
<p>If the button doesn't work, copy and paste this link into your browser: {{verificationUrl}}</p>
|
||||||
|
<p><strong>This link will expire in 24 hours.</strong></p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
passwordResetToUser: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{recipientName}},</p>
|
||||||
|
<h2>Reset Your Password</h2>
|
||||||
|
<p>We received a request to reset the password for your RentAll account. Click the button below to choose a new password.</p>
|
||||||
|
<p><a href="{{resetUrl}}" class="button">Reset Password</a></p>
|
||||||
|
<p>If the button doesn't work, copy and paste this link into your browser: {{resetUrl}}</p>
|
||||||
|
<p><strong>This link will expire in 1 hour.</strong></p>
|
||||||
|
<p>If you didn't request this, you can safely ignore this email.</p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
passwordChangedToUser: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{recipientName}},</p>
|
||||||
|
<h2>Your Password Has Been Changed</h2>
|
||||||
|
<p>This is a confirmation that the password for your RentAll account ({{email}}) has been successfully changed.</p>
|
||||||
|
<p><strong>Changed on:</strong> {{timestamp}}</p>
|
||||||
|
<p>For your security, all existing sessions have been logged out.</p>
|
||||||
|
<p><strong>Didn't change your password?</strong> If you did not make this change, please contact our support team immediately.</p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
rentalRequestToOwner: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{ownerName}},</p>
|
||||||
|
<h2>New Rental Request for {{itemName}}</h2>
|
||||||
|
<p>{{renterName}} would like to rent your item.</p>
|
||||||
|
<p><strong>Rental Period:</strong> {{startDate}} to {{endDate}}</p>
|
||||||
|
<p><strong>Total Amount:</strong> ${{totalAmount}}</p>
|
||||||
|
<p><strong>Your Earnings:</strong> ${{payoutAmount}}</p>
|
||||||
|
<p><strong>Delivery Method:</strong> {{deliveryMethod}}</p>
|
||||||
|
<p><strong>Renter Notes:</strong> {{rentalNotes}}</p>
|
||||||
|
<p><a href="{{approveUrl}}" class="button">Review & Respond</a></p>
|
||||||
|
<p>Please respond to this request within 24 hours.</p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
rentalRequestConfirmationToRenter: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{renterName}},</p>
|
||||||
|
<h2>Your Rental Request Has Been Submitted!</h2>
|
||||||
|
<p>Your request to rent <strong>{{itemName}}</strong> has been sent to the owner.</p>
|
||||||
|
<p><strong>Item:</strong> {{itemName}}</p>
|
||||||
|
<p><strong>Rental Period:</strong> {{startDate}} to {{endDate}}</p>
|
||||||
|
<p><strong>Delivery Method:</strong> {{deliveryMethod}}</p>
|
||||||
|
<p><strong>Total Amount:</strong> ${{totalAmount}}</p>
|
||||||
|
<p>{{paymentMessage}}</p>
|
||||||
|
<p>You'll receive an email notification once the owner responds to your request.</p>
|
||||||
|
<p><a href="{{viewRentalsUrl}}" class="button">View My Rentals</a></p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
rentalCancellationConfirmationToUser: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{recipientName}},</p>
|
||||||
|
<h2>Rental Cancelled Successfully</h2>
|
||||||
|
<p>This confirms that your rental for <strong>{{itemName}}</strong> has been cancelled.</p>
|
||||||
|
<p><strong>Item:</strong> {{itemName}}</p>
|
||||||
|
<p><strong>Start Date:</strong> {{startDate}}</p>
|
||||||
|
<p><strong>End Date:</strong> {{endDate}}</p>
|
||||||
|
<p><strong>Cancelled On:</strong> {{cancelledAt}}</p>
|
||||||
|
{{refundSection}}
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
rentalCancellationNotificationToUser: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{recipientName}},</p>
|
||||||
|
<h2>Rental Cancellation Notice</h2>
|
||||||
|
<p>{{cancellationMessage}}</p>
|
||||||
|
<p><strong>Item:</strong> {{itemName}}</p>
|
||||||
|
<p><strong>Start Date:</strong> {{startDate}}</p>
|
||||||
|
<p><strong>End Date:</strong> {{endDate}}</p>
|
||||||
|
<p><strong>Cancelled On:</strong> {{cancelledAt}}</p>
|
||||||
|
{{additionalInfo}}
|
||||||
|
<p>If you have any questions or concerns, please reach out to our support team.</p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
payoutReceivedToOwner: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{ownerName}},</p>
|
||||||
|
<h2 style="color: #28a745;">Earnings Received: ${{payoutAmount}}</h2>
|
||||||
|
<p>Great news! Your earnings from the rental of <strong>{{itemName}}</strong> have been transferred to your account.</p>
|
||||||
|
<h3>Rental Details</h3>
|
||||||
|
<p><strong>Item:</strong> {{itemName}}</p>
|
||||||
|
<p><strong>Rental Period:</strong> {{startDate}} to {{endDate}}</p>
|
||||||
|
<p><strong>Transfer ID:</strong> {{stripeTransferId}}</p>
|
||||||
|
<h3>Earnings Breakdown</h3>
|
||||||
|
<p><strong>Rental Amount:</strong> ${{totalAmount}}</p>
|
||||||
|
<p><strong>Community Upkeep Fee (10%):</strong> -${{platformFee}}</p>
|
||||||
|
<p style="font-size: 18px; color: #28a745;"><strong>Your Earnings:</strong> ${{payoutAmount}}</p>
|
||||||
|
<p>Funds are typically available in your bank account within 2-3 business days.</p>
|
||||||
|
<p><a href="{{earningsDashboardUrl}}" class="button">View Earnings Dashboard</a></p>
|
||||||
|
<p>Thank you for being a valued member of the RentAll community!</p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
rentalDeclinedToRenter: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{renterName}},</p>
|
||||||
|
<h2>Rental Request Declined</h2>
|
||||||
|
<p>Thank you for your interest in renting <strong>{{itemName}}</strong>. Unfortunately, the owner is unable to accept your rental request at this time.</p>
|
||||||
|
<h3>Request Details</h3>
|
||||||
|
<p><strong>Item:</strong> {{itemName}}</p>
|
||||||
|
<p><strong>Start Date:</strong> {{startDate}}</p>
|
||||||
|
<p><strong>End Date:</strong> {{endDate}}</p>
|
||||||
|
<p><strong>Delivery Method:</strong> {{deliveryMethod}}</p>
|
||||||
|
{{ownerMessage}}
|
||||||
|
<div class="info-box">
|
||||||
|
<p><strong>What happens next?</strong></p>
|
||||||
|
<p>{{paymentMessage}}</p>
|
||||||
|
<p>We encourage you to explore other similar items available for rent on RentAll. There are many great options waiting for you!</p>
|
||||||
|
</div>
|
||||||
|
<p style="text-align: center;"><a href="{{browseItemsUrl}}" class="button">Browse Available Items</a></p>
|
||||||
|
<p>If you have any questions or concerns, please don't hesitate to contact our support team.</p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
rentalApprovalConfirmationToOwner: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{ownerName}},</p>
|
||||||
|
<h2>You've Approved the Rental Request!</h2>
|
||||||
|
<p>You've successfully approved the rental request for <strong>{{itemName}}</strong>.</p>
|
||||||
|
<h3>Rental Details</h3>
|
||||||
|
<p><strong>Item:</strong> {{itemName}}</p>
|
||||||
|
<p><strong>Renter:</strong> {{renterName}}</p>
|
||||||
|
<p><strong>Start Date:</strong> {{startDate}}</p>
|
||||||
|
<p><strong>End Date:</strong> {{endDate}}</p>
|
||||||
|
<p><strong>Your Earnings:</strong> ${{payoutAmount}}</p>
|
||||||
|
{{stripeSection}}
|
||||||
|
<h3>What's Next?</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Coordinate with the renter on pickup details</li>
|
||||||
|
<li>Take photos of the item's condition before handoff</li>
|
||||||
|
<li>Provide any care instructions or usage tips</li>
|
||||||
|
</ul>
|
||||||
|
<p><a href="{{rentalDetailsUrl}}" class="button">View Rental Details</a></p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
rentalCompletionThankYouToRenter: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{renterName}},</p>
|
||||||
|
<h2>Thank You for Returning On Time!</h2>
|
||||||
|
<p>You've successfully returned <strong>{{itemName}}</strong> on time. On-time returns like yours help build trust in the RentAll community!</p>
|
||||||
|
<h3>Rental Summary</h3>
|
||||||
|
<p><strong>Item:</strong> {{itemName}}</p>
|
||||||
|
<p><strong>Rental Period:</strong> {{startDate}} to {{endDate}}</p>
|
||||||
|
<p><strong>Returned On:</strong> {{returnedDate}}</p>
|
||||||
|
{{reviewSection}}
|
||||||
|
<p><a href="{{browseItemsUrl}}" class="button">Browse Available Items</a></p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
rentalCompletionCongratsToOwner: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{ownerName}},</p>
|
||||||
|
<h2>Congratulations on Completing a Rental!</h2>
|
||||||
|
<p><strong>{{itemName}}</strong> has been successfully returned on time. Great job!</p>
|
||||||
|
<h3>Rental Summary</h3>
|
||||||
|
<p><strong>Item:</strong> {{itemName}}</p>
|
||||||
|
<p><strong>Renter:</strong> {{renterName}}</p>
|
||||||
|
<p><strong>Rental Period:</strong> {{startDate}} to {{endDate}}</p>
|
||||||
|
{{earningsSection}}
|
||||||
|
{{stripeSection}}
|
||||||
|
<p><a href="{{owningUrl}}" class="button">View My Listings</a></p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
feedbackConfirmationToUser: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<p>Hi {{userName}},</p>
|
||||||
|
<h2>Thank You for Your Feedback!</h2>
|
||||||
|
<p>We've received your feedback and our team will review it carefully.</p>
|
||||||
|
<div style="background-color: #f8f9fa; border-left: 4px solid #007bff; padding: 15px; margin: 20px 0; font-style: italic;">
|
||||||
|
{{feedbackText}}
|
||||||
|
</div>
|
||||||
|
<p><strong>Submitted:</strong> {{submittedAt}}</p>
|
||||||
|
<p>Your input helps us improve RentAll for everyone. We take all feedback seriously and use it to make the platform better.</p>
|
||||||
|
<p>If your feedback requires a response, our team will reach out to you directly.</p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
|
||||||
|
feedbackNotificationToAdmin: baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<h2>New Feedback Received</h2>
|
||||||
|
<p><strong>From:</strong> {{userName}} ({{userEmail}})</p>
|
||||||
|
<p><strong>User ID:</strong> {{userId}}</p>
|
||||||
|
<p><strong>Submitted:</strong> {{submittedAt}}</p>
|
||||||
|
<h3>Feedback Content</h3>
|
||||||
|
<div style="background-color: #e7f3ff; border-left: 4px solid #007bff; padding: 20px; margin: 20px 0;">
|
||||||
|
{{feedbackText}}
|
||||||
|
</div>
|
||||||
|
<h3>Technical Context</h3>
|
||||||
|
<p><strong>Feedback ID:</strong> {{feedbackId}}</p>
|
||||||
|
<p><strong>Page URL:</strong> {{url}}</p>
|
||||||
|
<p><strong>User Agent:</strong> {{userAgent}}</p>
|
||||||
|
<p>Please review this feedback and take appropriate action if needed.</p>
|
||||||
|
`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
templates[templateName] ||
|
||||||
|
baseTemplate.replace(
|
||||||
|
"{{content}}",
|
||||||
|
`
|
||||||
|
<h2>{{title}}</h2>
|
||||||
|
<p>{{message}}</p>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TemplateManager;
|
||||||
98
backend/services/email/core/emailUtils.js
Normal file
98
backend/services/email/core/emailUtils.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Email utility functions shared across all email services
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert HTML to plain text for email fallback
|
||||||
|
* Strips HTML tags and formats content for plain text email clients
|
||||||
|
* @param {string} html - HTML content to convert
|
||||||
|
* @returns {string} Plain text version of the HTML
|
||||||
|
*/
|
||||||
|
function htmlToPlainText(html) {
|
||||||
|
return (
|
||||||
|
html
|
||||||
|
// Remove style and script tags and their content
|
||||||
|
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
||||||
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
||||||
|
// Convert common HTML elements to text equivalents
|
||||||
|
.replace(/<br\s*\/?>/gi, "\n")
|
||||||
|
.replace(/<\/p>/gi, "\n\n")
|
||||||
|
.replace(/<\/div>/gi, "\n")
|
||||||
|
.replace(/<\/li>/gi, "\n")
|
||||||
|
.replace(/<\/h[1-6]>/gi, "\n\n")
|
||||||
|
.replace(/<li>/gi, "• ")
|
||||||
|
// Remove remaining HTML tags
|
||||||
|
.replace(/<[^>]+>/g, "")
|
||||||
|
// Decode HTML entities
|
||||||
|
.replace(/ /g, " ")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
// Remove emojis and special characters that don't render well in plain text
|
||||||
|
.replace(/[\u{1F600}-\u{1F64F}]/gu, "") // Emoticons
|
||||||
|
.replace(/[\u{1F300}-\u{1F5FF}]/gu, "") // Misc Symbols and Pictographs
|
||||||
|
.replace(/[\u{1F680}-\u{1F6FF}]/gu, "") // Transport and Map
|
||||||
|
.replace(/[\u{2600}-\u{26FF}]/gu, "") // Misc symbols
|
||||||
|
.replace(/[\u{2700}-\u{27BF}]/gu, "") // Dingbats
|
||||||
|
.replace(/[\u{FE00}-\u{FE0F}]/gu, "") // Variation Selectors
|
||||||
|
.replace(/[\u{1F900}-\u{1F9FF}]/gu, "") // Supplemental Symbols and Pictographs
|
||||||
|
.replace(/[\u{1FA70}-\u{1FAFF}]/gu, "") // Symbols and Pictographs Extended-A
|
||||||
|
// Clean up excessive whitespace
|
||||||
|
.replace(/\n\s*\n\s*\n/g, "\n\n")
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date consistently for email display
|
||||||
|
* @param {Date|string} date - Date to format
|
||||||
|
* @returns {string} Formatted date string
|
||||||
|
*/
|
||||||
|
function formatEmailDate(date) {
|
||||||
|
const dateObj = typeof date === "string" ? new Date(date) : date;
|
||||||
|
return dateObj.toLocaleString("en-US", {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date as a short date (no time)
|
||||||
|
* @param {Date|string} date - Date to format
|
||||||
|
* @returns {string} Formatted date string
|
||||||
|
*/
|
||||||
|
function formatShortDate(date) {
|
||||||
|
const dateObj = typeof date === "string" ? new Date(date) : date;
|
||||||
|
return dateObj.toLocaleString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format currency for email display
|
||||||
|
* @param {number} amount - Amount in cents or smallest currency unit
|
||||||
|
* @param {string} currency - Currency code (default: USD)
|
||||||
|
* @returns {string} Formatted currency string
|
||||||
|
*/
|
||||||
|
function formatCurrency(amount, currency = "USD") {
|
||||||
|
return new Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: currency,
|
||||||
|
}).format(amount / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
htmlToPlainText,
|
||||||
|
formatEmailDate,
|
||||||
|
formatShortDate,
|
||||||
|
formatCurrency,
|
||||||
|
};
|
||||||
71
backend/services/email/domain/AlphaInvitationEmailService.js
Normal file
71
backend/services/email/domain/AlphaInvitationEmailService.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
const EmailClient = require("../core/EmailClient");
|
||||||
|
const TemplateManager = require("../core/TemplateManager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AlphaInvitationEmailService handles alpha program invitation emails
|
||||||
|
* This service is responsible for:
|
||||||
|
* - Sending alpha access invitation codes to new testers
|
||||||
|
*/
|
||||||
|
class AlphaInvitationEmailService {
|
||||||
|
constructor() {
|
||||||
|
this.emailClient = new EmailClient();
|
||||||
|
this.templateManager = new TemplateManager();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the alpha invitation email service
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.emailClient.initialize(),
|
||||||
|
this.templateManager.initialize(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
console.log("Alpha Invitation Email Service initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send alpha invitation email
|
||||||
|
* @param {string} email - Recipient's email address
|
||||||
|
* @param {string} code - Alpha access code
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendAlphaInvitation(email, code) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
code: code,
|
||||||
|
email: email,
|
||||||
|
frontendUrl: frontendUrl,
|
||||||
|
title: "Welcome to Alpha Testing!",
|
||||||
|
message: `You've been invited to join our exclusive alpha testing program. Use the code <strong>${code}</strong> to unlock access and be among the first to experience our platform.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"alphaInvitationToUser",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
return await this.emailClient.sendEmail(
|
||||||
|
email,
|
||||||
|
"Your Alpha Access Code - RentAll",
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send alpha invitation email:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AlphaInvitationEmailService;
|
||||||
136
backend/services/email/domain/AuthEmailService.js
Normal file
136
backend/services/email/domain/AuthEmailService.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
const EmailClient = require("../core/EmailClient");
|
||||||
|
const TemplateManager = require("../core/TemplateManager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuthEmailService handles all authentication and account security related emails
|
||||||
|
* This service is responsible for:
|
||||||
|
* - Sending email verification links
|
||||||
|
* - Sending password reset links
|
||||||
|
* - Sending password changed confirmations
|
||||||
|
*/
|
||||||
|
class AuthEmailService {
|
||||||
|
constructor() {
|
||||||
|
this.emailClient = new EmailClient();
|
||||||
|
this.templateManager = new TemplateManager();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the auth email service
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.emailClient.initialize(),
|
||||||
|
this.templateManager.initialize(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
console.log("Auth Email Service initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send email verification email to new users
|
||||||
|
* @param {Object} user - User object
|
||||||
|
* @param {string} user.firstName - User's first name
|
||||||
|
* @param {string} user.email - User's email address
|
||||||
|
* @param {string} verificationToken - Email verification token
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendVerificationEmail(user, verificationToken) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||||
|
const verificationUrl = `${frontendUrl}/verify-email?token=${verificationToken}`;
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
recipientName: user.firstName || "there",
|
||||||
|
verificationUrl: verificationUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"emailVerificationToUser",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
return await this.emailClient.sendEmail(
|
||||||
|
user.email,
|
||||||
|
"Verify Your Email - RentAll",
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send password reset email with reset link
|
||||||
|
* @param {Object} user - User object
|
||||||
|
* @param {string} user.firstName - User's first name
|
||||||
|
* @param {string} user.email - User's email address
|
||||||
|
* @param {string} resetToken - Password reset token
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendPasswordResetEmail(user, resetToken) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||||
|
const resetUrl = `${frontendUrl}/reset-password?token=${resetToken}`;
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
recipientName: user.firstName || "there",
|
||||||
|
resetUrl: resetUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"passwordResetToUser",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
return await this.emailClient.sendEmail(
|
||||||
|
user.email,
|
||||||
|
"Reset Your Password - RentAll",
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send password changed confirmation email
|
||||||
|
* @param {Object} user - User object
|
||||||
|
* @param {string} user.firstName - User's first name
|
||||||
|
* @param {string} user.email - User's email address
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendPasswordChangedEmail(user) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = new Date().toLocaleString("en-US", {
|
||||||
|
dateStyle: "long",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
recipientName: user.firstName || "there",
|
||||||
|
email: user.email,
|
||||||
|
timestamp: timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"passwordChangedToUser",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
return await this.emailClient.sendEmail(
|
||||||
|
user.email,
|
||||||
|
"Password Changed Successfully - RentAll",
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AuthEmailService;
|
||||||
299
backend/services/email/domain/CustomerServiceEmailService.js
Normal file
299
backend/services/email/domain/CustomerServiceEmailService.js
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
const EmailClient = require("../core/EmailClient");
|
||||||
|
const TemplateManager = require("../core/TemplateManager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CustomerServiceEmailService handles all customer service alert emails
|
||||||
|
* This service is responsible for:
|
||||||
|
* - Sending late return notifications to CS team
|
||||||
|
* - Sending damage report notifications to CS team
|
||||||
|
* - Sending lost item notifications to CS team
|
||||||
|
*/
|
||||||
|
class CustomerServiceEmailService {
|
||||||
|
constructor() {
|
||||||
|
this.emailClient = new EmailClient();
|
||||||
|
this.templateManager = new TemplateManager();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the customer service email service
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.emailClient.initialize(),
|
||||||
|
this.templateManager.initialize(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
console.log("Customer Service Email Service initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send late return notification to customer service
|
||||||
|
* @param {Object} rental - Rental object
|
||||||
|
* @param {number} rental.id - Rental ID
|
||||||
|
* @param {Date} rental.endDateTime - Scheduled end date/time
|
||||||
|
* @param {Date} rental.actualReturnDateTime - Actual return date/time
|
||||||
|
* @param {Object} rental.item - Item object with name property
|
||||||
|
* @param {Object} owner - Owner user object
|
||||||
|
* @param {string} owner.firstName - Owner's first name
|
||||||
|
* @param {string} owner.lastName - Owner's last name
|
||||||
|
* @param {string} owner.email - Owner's email
|
||||||
|
* @param {Object} renter - Renter user object
|
||||||
|
* @param {string} renter.firstName - Renter's first name
|
||||||
|
* @param {string} renter.lastName - Renter's last name
|
||||||
|
* @param {string} renter.email - Renter's email
|
||||||
|
* @param {Object} lateCalculation - Late fee calculation
|
||||||
|
* @param {number} lateCalculation.lateHours - Hours late
|
||||||
|
* @param {number} lateCalculation.lateFee - Late fee amount
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendLateReturnToCustomerService(rental, owner, renter, lateCalculation) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csEmail = process.env.CUSTOMER_SUPPORT_EMAIL;
|
||||||
|
if (!csEmail) {
|
||||||
|
console.warn("No customer service email configured");
|
||||||
|
return { success: false, error: "No customer service email configured" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format dates
|
||||||
|
const scheduledEnd = new Date(rental.endDateTime).toLocaleString();
|
||||||
|
const actualReturn = new Date(rental.actualReturnDateTime).toLocaleString();
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
rentalId: rental.id,
|
||||||
|
itemName: rental.item.name,
|
||||||
|
ownerName: `${owner.firstName} ${owner.lastName}`,
|
||||||
|
ownerEmail: owner.email,
|
||||||
|
renterName: `${renter.firstName} ${renter.lastName}`,
|
||||||
|
renterEmail: renter.email,
|
||||||
|
scheduledEnd,
|
||||||
|
actualReturn,
|
||||||
|
hoursLate: lateCalculation.lateHours.toFixed(1),
|
||||||
|
lateFee: lateCalculation.lateFee.toFixed(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"lateReturnToCS",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await this.emailClient.sendEmail(
|
||||||
|
csEmail,
|
||||||
|
"Late Return Detected - Action Required",
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(
|
||||||
|
`Late return notification sent to customer service for rental ${rental.id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Failed to send late return notification to customer service:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send damage report notification to customer service
|
||||||
|
* @param {Object} rental - Rental object
|
||||||
|
* @param {number} rental.id - Rental ID
|
||||||
|
* @param {Object} rental.item - Item object with name property
|
||||||
|
* @param {Object} owner - Owner user object
|
||||||
|
* @param {string} owner.firstName - Owner's first name
|
||||||
|
* @param {string} owner.lastName - Owner's last name
|
||||||
|
* @param {string} owner.email - Owner's email
|
||||||
|
* @param {Object} renter - Renter user object
|
||||||
|
* @param {string} renter.firstName - Renter's first name
|
||||||
|
* @param {string} renter.lastName - Renter's last name
|
||||||
|
* @param {string} renter.email - Renter's email
|
||||||
|
* @param {Object} damageAssessment - Damage assessment details
|
||||||
|
* @param {string} damageAssessment.description - Damage description
|
||||||
|
* @param {boolean} damageAssessment.canBeFixed - Whether item can be repaired
|
||||||
|
* @param {number} [damageAssessment.repairCost] - Repair cost if applicable
|
||||||
|
* @param {boolean} damageAssessment.needsReplacement - Whether item needs replacement
|
||||||
|
* @param {number} [damageAssessment.replacementCost] - Replacement cost if applicable
|
||||||
|
* @param {Object} damageAssessment.feeCalculation - Fee calculation details
|
||||||
|
* @param {string} damageAssessment.feeCalculation.type - Fee type (repair/replacement)
|
||||||
|
* @param {number} damageAssessment.feeCalculation.amount - Fee amount
|
||||||
|
* @param {Array} [damageAssessment.proofOfOwnership] - Proof of ownership documents
|
||||||
|
* @param {Object} [lateCalculation] - Late fee calculation (optional)
|
||||||
|
* @param {number} [lateCalculation.lateFee] - Late fee amount
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendDamageReportToCustomerService(
|
||||||
|
rental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
|
damageAssessment,
|
||||||
|
lateCalculation = null
|
||||||
|
) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csEmail = process.env.CUSTOMER_SUPPORT_EMAIL;
|
||||||
|
if (!csEmail) {
|
||||||
|
console.warn("No customer service email configured");
|
||||||
|
return { success: false, error: "No customer service email configured" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total fees (ensure numeric values)
|
||||||
|
const damageFee = parseFloat(damageAssessment.feeCalculation.amount) || 0;
|
||||||
|
const lateFee = parseFloat(lateCalculation?.lateFee || 0);
|
||||||
|
const totalFees = damageFee + lateFee;
|
||||||
|
|
||||||
|
// Determine fee type description
|
||||||
|
let feeTypeDescription = "";
|
||||||
|
if (damageAssessment.feeCalculation.type === "repair") {
|
||||||
|
feeTypeDescription = "Repair Cost";
|
||||||
|
} else if (damageAssessment.feeCalculation.type === "replacement") {
|
||||||
|
feeTypeDescription = "Replacement Cost";
|
||||||
|
} else {
|
||||||
|
feeTypeDescription = "Damage Assessment Fee";
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
rentalId: rental.id,
|
||||||
|
itemName: rental.item.name,
|
||||||
|
ownerName: `${owner.firstName} ${owner.lastName}`,
|
||||||
|
ownerEmail: owner.email,
|
||||||
|
renterName: `${renter.firstName} ${renter.lastName}`,
|
||||||
|
renterEmail: renter.email,
|
||||||
|
damageDescription: damageAssessment.description,
|
||||||
|
canBeFixed: damageAssessment.canBeFixed ? "Yes" : "No",
|
||||||
|
repairCost: damageAssessment.repairCost
|
||||||
|
? damageAssessment.repairCost.toFixed(2)
|
||||||
|
: "N/A",
|
||||||
|
needsReplacement: damageAssessment.needsReplacement ? "Yes" : "No",
|
||||||
|
replacementCost: damageAssessment.replacementCost
|
||||||
|
? damageAssessment.replacementCost.toFixed(2)
|
||||||
|
: "N/A",
|
||||||
|
feeTypeDescription,
|
||||||
|
damageFee: damageFee.toFixed(2),
|
||||||
|
lateFee: lateFee.toFixed(2),
|
||||||
|
totalFees: totalFees.toFixed(2),
|
||||||
|
hasProofOfOwnership:
|
||||||
|
damageAssessment.proofOfOwnership &&
|
||||||
|
damageAssessment.proofOfOwnership.length > 0
|
||||||
|
? "Yes"
|
||||||
|
: "No",
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"damageReportToCS",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await this.emailClient.sendEmail(
|
||||||
|
csEmail,
|
||||||
|
"Damage Report Filed - Action Required",
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(
|
||||||
|
`Damage report notification sent to customer service for rental ${rental.id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Failed to send damage report notification to customer service:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send lost item notification to customer service
|
||||||
|
* @param {Object} rental - Rental object
|
||||||
|
* @param {number} rental.id - Rental ID
|
||||||
|
* @param {Date} rental.endDateTime - Scheduled return date
|
||||||
|
* @param {Date} rental.itemLostReportedAt - When loss was reported
|
||||||
|
* @param {Object} rental.item - Item object
|
||||||
|
* @param {string} rental.item.name - Item name
|
||||||
|
* @param {number} rental.item.replacementCost - Item replacement cost
|
||||||
|
* @param {Object} owner - Owner user object
|
||||||
|
* @param {string} owner.firstName - Owner's first name
|
||||||
|
* @param {string} owner.lastName - Owner's last name
|
||||||
|
* @param {string} owner.email - Owner's email
|
||||||
|
* @param {Object} renter - Renter user object
|
||||||
|
* @param {string} renter.firstName - Renter's first name
|
||||||
|
* @param {string} renter.lastName - Renter's last name
|
||||||
|
* @param {string} renter.email - Renter's email
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendLostItemToCustomerService(rental, owner, renter) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csEmail = process.env.CUSTOMER_SUPPORT_EMAIL;
|
||||||
|
if (!csEmail) {
|
||||||
|
console.warn("No customer service email configured");
|
||||||
|
return { success: false, error: "No customer service email configured" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format dates
|
||||||
|
const reportedAt = new Date(rental.itemLostReportedAt).toLocaleString();
|
||||||
|
const scheduledReturnDate = new Date(rental.endDateTime).toLocaleString();
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
rentalId: rental.id,
|
||||||
|
itemName: rental.item.name,
|
||||||
|
ownerName: `${owner.firstName} ${owner.lastName}`,
|
||||||
|
ownerEmail: owner.email,
|
||||||
|
renterName: `${renter.firstName} ${renter.lastName}`,
|
||||||
|
renterEmail: renter.email,
|
||||||
|
reportedAt,
|
||||||
|
scheduledReturnDate,
|
||||||
|
replacementCost: parseFloat(rental.item.replacementCost).toFixed(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"lostItemToCS",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await this.emailClient.sendEmail(
|
||||||
|
csEmail,
|
||||||
|
"Lost Item Claim Filed - Action Required",
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(
|
||||||
|
`Lost item notification sent to customer service for rental ${rental.id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Failed to send lost item notification to customer service:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CustomerServiceEmailService;
|
||||||
131
backend/services/email/domain/FeedbackEmailService.js
Normal file
131
backend/services/email/domain/FeedbackEmailService.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
const EmailClient = require("../core/EmailClient");
|
||||||
|
const TemplateManager = require("../core/TemplateManager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FeedbackEmailService handles all feedback-related email notifications
|
||||||
|
* This service is responsible for:
|
||||||
|
* - Sending feedback confirmation to users
|
||||||
|
* - Sending feedback notifications to administrators
|
||||||
|
*/
|
||||||
|
class FeedbackEmailService {
|
||||||
|
constructor() {
|
||||||
|
this.emailClient = new EmailClient();
|
||||||
|
this.templateManager = new TemplateManager();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the feedback email service
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.emailClient.initialize(),
|
||||||
|
this.templateManager.initialize(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
console.log("Feedback Email Service initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send feedback confirmation email to user
|
||||||
|
* @param {Object} user - User object
|
||||||
|
* @param {string} user.firstName - User's first name
|
||||||
|
* @param {string} user.email - User's email address
|
||||||
|
* @param {Object} feedback - Feedback object
|
||||||
|
* @param {string} feedback.feedbackText - The feedback content
|
||||||
|
* @param {Date} feedback.createdAt - Feedback submission timestamp
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendFeedbackConfirmation(user, feedback) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const submittedAt = new Date(feedback.createdAt).toLocaleString("en-US", {
|
||||||
|
dateStyle: "long",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
userName: user.firstName || "there",
|
||||||
|
userEmail: user.email,
|
||||||
|
feedbackText: feedback.feedbackText,
|
||||||
|
submittedAt: submittedAt,
|
||||||
|
year: new Date().getFullYear(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"feedbackConfirmationToUser",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
return await this.emailClient.sendEmail(
|
||||||
|
user.email,
|
||||||
|
"Thank You for Your Feedback - RentAll",
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send feedback notification to admin
|
||||||
|
* @param {Object} user - User object who submitted feedback
|
||||||
|
* @param {string} user.firstName - User's first name
|
||||||
|
* @param {string} user.lastName - User's last name
|
||||||
|
* @param {string} user.email - User's email address
|
||||||
|
* @param {string} user.id - User's ID
|
||||||
|
* @param {Object} feedback - Feedback object
|
||||||
|
* @param {string} feedback.id - Feedback ID
|
||||||
|
* @param {string} feedback.feedbackText - The feedback content
|
||||||
|
* @param {string} [feedback.url] - URL where feedback was submitted
|
||||||
|
* @param {string} [feedback.userAgent] - User's browser user agent
|
||||||
|
* @param {Date} feedback.createdAt - Feedback submission timestamp
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendFeedbackNotificationToAdmin(user, feedback) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminEmail =
|
||||||
|
process.env.FEEDBACK_EMAIL || process.env.CUSTOMER_SUPPORT_EMAIL;
|
||||||
|
|
||||||
|
if (!adminEmail) {
|
||||||
|
console.warn("No admin email configured for feedback notifications");
|
||||||
|
return { success: false, error: "No admin email configured" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const submittedAt = new Date(feedback.createdAt).toLocaleString("en-US", {
|
||||||
|
dateStyle: "long",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
userName: `${user.firstName} ${user.lastName}`.trim() || "Unknown User",
|
||||||
|
userEmail: user.email,
|
||||||
|
userId: user.id,
|
||||||
|
feedbackText: feedback.feedbackText,
|
||||||
|
feedbackId: feedback.id,
|
||||||
|
url: feedback.url || "Not provided",
|
||||||
|
userAgent: feedback.userAgent || "Not provided",
|
||||||
|
submittedAt: submittedAt,
|
||||||
|
year: new Date().getFullYear(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"feedbackNotificationToAdmin",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
return await this.emailClient.sendEmail(
|
||||||
|
adminEmail,
|
||||||
|
`New Feedback from ${user.firstName} ${user.lastName}`,
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FeedbackEmailService;
|
||||||
318
backend/services/email/domain/ForumEmailService.js
Normal file
318
backend/services/email/domain/ForumEmailService.js
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
const EmailClient = require("../core/EmailClient");
|
||||||
|
const TemplateManager = require("../core/TemplateManager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ForumEmailService handles all forum-related email notifications
|
||||||
|
* This service is responsible for:
|
||||||
|
* - Sending comment notifications to post authors
|
||||||
|
* - Sending reply notifications to comment authors
|
||||||
|
* - Sending answer accepted notifications
|
||||||
|
* - Sending thread activity notifications to participants
|
||||||
|
*/
|
||||||
|
class ForumEmailService {
|
||||||
|
constructor() {
|
||||||
|
this.emailClient = new EmailClient();
|
||||||
|
this.templateManager = new TemplateManager();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the forum email service
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.emailClient.initialize(),
|
||||||
|
this.templateManager.initialize(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
console.log("Forum Email Service initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification when someone comments on a post
|
||||||
|
* @param {Object} postAuthor - Post author user object
|
||||||
|
* @param {string} postAuthor.firstName - Post author's first name
|
||||||
|
* @param {string} postAuthor.email - Post author's email
|
||||||
|
* @param {Object} commenter - Commenter user object
|
||||||
|
* @param {string} commenter.firstName - Commenter's first name
|
||||||
|
* @param {string} commenter.lastName - Commenter's last name
|
||||||
|
* @param {Object} post - Forum post object
|
||||||
|
* @param {number} post.id - Post ID
|
||||||
|
* @param {string} post.title - Post title
|
||||||
|
* @param {Object} comment - Comment object
|
||||||
|
* @param {string} comment.content - Comment content
|
||||||
|
* @param {Date} comment.createdAt - Comment creation timestamp
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendForumCommentNotification(postAuthor, commenter, post, comment) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||||
|
const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
|
||||||
|
|
||||||
|
const timestamp = new Date(comment.createdAt).toLocaleString("en-US", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
postAuthorName: postAuthor.firstName || "there",
|
||||||
|
commenterName:
|
||||||
|
`${commenter.firstName} ${commenter.lastName}`.trim() || "Someone",
|
||||||
|
postTitle: post.title,
|
||||||
|
commentContent: comment.content,
|
||||||
|
postUrl: postUrl,
|
||||||
|
timestamp: timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"forumCommentToPostAuthor",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
const subject = `${commenter.firstName} ${commenter.lastName} commented on your post`;
|
||||||
|
|
||||||
|
const result = await this.emailClient.sendEmail(
|
||||||
|
postAuthor.email,
|
||||||
|
subject,
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(
|
||||||
|
`Forum comment notification email sent to ${postAuthor.email}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send forum comment notification email:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification when someone replies to a comment
|
||||||
|
* @param {Object} commentAuthor - Original comment author user object
|
||||||
|
* @param {string} commentAuthor.firstName - Comment author's first name
|
||||||
|
* @param {string} commentAuthor.email - Comment author's email
|
||||||
|
* @param {Object} replier - Replier user object
|
||||||
|
* @param {string} replier.firstName - Replier's first name
|
||||||
|
* @param {string} replier.lastName - Replier's last name
|
||||||
|
* @param {Object} post - Forum post object
|
||||||
|
* @param {number} post.id - Post ID
|
||||||
|
* @param {string} post.title - Post title
|
||||||
|
* @param {Object} reply - Reply comment object
|
||||||
|
* @param {string} reply.content - Reply content
|
||||||
|
* @param {Date} reply.createdAt - Reply creation timestamp
|
||||||
|
* @param {Object} parentComment - Parent comment being replied to
|
||||||
|
* @param {string} parentComment.content - Parent comment content
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendForumReplyNotification(
|
||||||
|
commentAuthor,
|
||||||
|
replier,
|
||||||
|
post,
|
||||||
|
reply,
|
||||||
|
parentComment
|
||||||
|
) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||||
|
const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
|
||||||
|
|
||||||
|
const timestamp = new Date(reply.createdAt).toLocaleString("en-US", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
commentAuthorName: commentAuthor.firstName || "there",
|
||||||
|
replierName:
|
||||||
|
`${replier.firstName} ${replier.lastName}`.trim() || "Someone",
|
||||||
|
postTitle: post.title,
|
||||||
|
parentCommentContent: parentComment.content,
|
||||||
|
replyContent: reply.content,
|
||||||
|
postUrl: postUrl,
|
||||||
|
timestamp: timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"forumReplyToCommentAuthor",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
const subject = `${replier.firstName} ${replier.lastName} replied to your comment`;
|
||||||
|
|
||||||
|
const result = await this.emailClient.sendEmail(
|
||||||
|
commentAuthor.email,
|
||||||
|
subject,
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(
|
||||||
|
`Forum reply notification email sent to ${commentAuthor.email}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send forum reply notification email:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification when a comment is marked as the accepted answer
|
||||||
|
* @param {Object} commentAuthor - Comment author user object
|
||||||
|
* @param {string} commentAuthor.firstName - Comment author's first name
|
||||||
|
* @param {string} commentAuthor.email - Comment author's email
|
||||||
|
* @param {Object} postAuthor - Post author user object who accepted the answer
|
||||||
|
* @param {string} postAuthor.firstName - Post author's first name
|
||||||
|
* @param {string} postAuthor.lastName - Post author's last name
|
||||||
|
* @param {Object} post - Forum post object
|
||||||
|
* @param {number} post.id - Post ID
|
||||||
|
* @param {string} post.title - Post title
|
||||||
|
* @param {Object} comment - Comment that was accepted as answer
|
||||||
|
* @param {string} comment.content - Comment content
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendForumAnswerAcceptedNotification(
|
||||||
|
commentAuthor,
|
||||||
|
postAuthor,
|
||||||
|
post,
|
||||||
|
comment
|
||||||
|
) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||||
|
const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
commentAuthorName: commentAuthor.firstName || "there",
|
||||||
|
postAuthorName:
|
||||||
|
`${postAuthor.firstName} ${postAuthor.lastName}`.trim() || "Someone",
|
||||||
|
postTitle: post.title,
|
||||||
|
commentContent: comment.content,
|
||||||
|
postUrl: postUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"forumAnswerAcceptedToCommentAuthor",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
const subject = `Your comment was marked as the accepted answer!`;
|
||||||
|
|
||||||
|
const result = await this.emailClient.sendEmail(
|
||||||
|
commentAuthor.email,
|
||||||
|
subject,
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(
|
||||||
|
`Forum answer accepted notification email sent to ${commentAuthor.email}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Failed to send forum answer accepted notification email:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification to thread participants about new activity
|
||||||
|
* @param {Object} participant - Participant user object
|
||||||
|
* @param {string} participant.firstName - Participant's first name
|
||||||
|
* @param {string} participant.email - Participant's email
|
||||||
|
* @param {Object} commenter - User who posted new comment
|
||||||
|
* @param {string} commenter.firstName - Commenter's first name
|
||||||
|
* @param {string} commenter.lastName - Commenter's last name
|
||||||
|
* @param {Object} post - Forum post object
|
||||||
|
* @param {number} post.id - Post ID
|
||||||
|
* @param {string} post.title - Post title
|
||||||
|
* @param {Object} comment - New comment/activity
|
||||||
|
* @param {string} comment.content - Comment content
|
||||||
|
* @param {Date} comment.createdAt - Comment creation timestamp
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendForumThreadActivityNotification(
|
||||||
|
participant,
|
||||||
|
commenter,
|
||||||
|
post,
|
||||||
|
comment
|
||||||
|
) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||||
|
const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
|
||||||
|
|
||||||
|
const timestamp = new Date(comment.createdAt).toLocaleString("en-US", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
participantName: participant.firstName || "there",
|
||||||
|
commenterName:
|
||||||
|
`${commenter.firstName} ${commenter.lastName}`.trim() || "Someone",
|
||||||
|
postTitle: post.title,
|
||||||
|
commentContent: comment.content,
|
||||||
|
postUrl: postUrl,
|
||||||
|
timestamp: timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"forumThreadActivityToParticipant",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
const subject = `New activity on a post you're following`;
|
||||||
|
|
||||||
|
const result = await this.emailClient.sendEmail(
|
||||||
|
participant.email,
|
||||||
|
subject,
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(
|
||||||
|
`Forum thread activity notification email sent to ${participant.email}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Failed to send forum thread activity notification email:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ForumEmailService;
|
||||||
97
backend/services/email/domain/MessagingEmailService.js
Normal file
97
backend/services/email/domain/MessagingEmailService.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
const EmailClient = require("../core/EmailClient");
|
||||||
|
const TemplateManager = require("../core/TemplateManager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MessagingEmailService handles all messaging-related email notifications
|
||||||
|
* This service is responsible for:
|
||||||
|
* - Sending new message notifications to users
|
||||||
|
*/
|
||||||
|
class MessagingEmailService {
|
||||||
|
constructor() {
|
||||||
|
this.emailClient = new EmailClient();
|
||||||
|
this.templateManager = new TemplateManager();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the messaging email service
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.emailClient.initialize(),
|
||||||
|
this.templateManager.initialize(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
console.log("Messaging Email Service initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send new message notification email
|
||||||
|
* @param {Object} receiver - User object of the message receiver
|
||||||
|
* @param {string} receiver.firstName - Receiver's first name
|
||||||
|
* @param {string} receiver.email - Receiver's email address
|
||||||
|
* @param {Object} sender - User object of the message sender
|
||||||
|
* @param {string} sender.id - Sender's user ID
|
||||||
|
* @param {string} sender.firstName - Sender's first name
|
||||||
|
* @param {string} sender.lastName - Sender's last name
|
||||||
|
* @param {Object} message - Message object
|
||||||
|
* @param {string} message.subject - Message subject
|
||||||
|
* @param {string} message.content - Message content
|
||||||
|
* @param {Date} message.createdAt - Message creation timestamp
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendNewMessageNotification(receiver, sender, message) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||||
|
const conversationUrl = `${frontendUrl}/messages/conversations/${sender.id}`;
|
||||||
|
|
||||||
|
const timestamp = new Date(message.createdAt).toLocaleString("en-US", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
recipientName: receiver.firstName || "there",
|
||||||
|
senderName: `${sender.firstName} ${sender.lastName}`.trim() || "A user",
|
||||||
|
subject: message.subject,
|
||||||
|
messageContent: message.content,
|
||||||
|
conversationUrl: conversationUrl,
|
||||||
|
timestamp: timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"newMessageToUser",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
const subject = `New message from ${sender.firstName} ${sender.lastName}`;
|
||||||
|
|
||||||
|
const result = await this.emailClient.sendEmail(
|
||||||
|
receiver.email,
|
||||||
|
subject,
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(
|
||||||
|
`Message notification email sent to ${receiver.email} from ${sender.firstName} ${sender.lastName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send message notification email:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MessagingEmailService;
|
||||||
1201
backend/services/email/domain/RentalFlowEmailService.js
Normal file
1201
backend/services/email/domain/RentalFlowEmailService.js
Normal file
File diff suppressed because it is too large
Load Diff
77
backend/services/email/domain/RentalReminderEmailService.js
Normal file
77
backend/services/email/domain/RentalReminderEmailService.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
const EmailClient = require("../core/EmailClient");
|
||||||
|
const TemplateManager = require("../core/TemplateManager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RentalReminderEmailService handles rental reminder emails
|
||||||
|
* This service is responsible for:
|
||||||
|
* - Sending condition check reminders
|
||||||
|
*/
|
||||||
|
class RentalReminderEmailService {
|
||||||
|
constructor() {
|
||||||
|
this.emailClient = new EmailClient();
|
||||||
|
this.templateManager = new TemplateManager();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the rental reminder email service
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.emailClient.initialize(),
|
||||||
|
this.templateManager.initialize(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
console.log("Rental Reminder Email Service initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send condition check reminder email
|
||||||
|
* @param {string} userEmail - User's email address
|
||||||
|
* @param {Object} notification - Notification object
|
||||||
|
* @param {string} notification.title - Notification title
|
||||||
|
* @param {string} notification.message - Notification message
|
||||||
|
* @param {Object} notification.metadata - Notification metadata
|
||||||
|
* @param {string} notification.metadata.deadline - Condition check deadline
|
||||||
|
* @param {Object} rental - Rental object
|
||||||
|
* @param {Object} rental.item - Item object
|
||||||
|
* @param {string} rental.item.name - Item name
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendConditionCheckReminder(userEmail, notification, rental) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const variables = {
|
||||||
|
title: notification.title,
|
||||||
|
message: notification.message,
|
||||||
|
itemName: rental?.item?.name || "Unknown Item",
|
||||||
|
deadline: notification.metadata?.deadline
|
||||||
|
? new Date(notification.metadata.deadline).toLocaleDateString()
|
||||||
|
: "Not specified",
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"conditionCheckReminderToUser",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
return await this.emailClient.sendEmail(
|
||||||
|
userEmail,
|
||||||
|
`RentAll: ${notification.title}`,
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send condition check reminder:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RentalReminderEmailService;
|
||||||
77
backend/services/email/domain/UserEngagementEmailService.js
Normal file
77
backend/services/email/domain/UserEngagementEmailService.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
const EmailClient = require("../core/EmailClient");
|
||||||
|
const TemplateManager = require("../core/TemplateManager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserEngagementEmailService handles user engagement emails
|
||||||
|
* This service is responsible for:
|
||||||
|
* - Sending first listing celebration emails
|
||||||
|
* - Other user engagement and milestone emails
|
||||||
|
*/
|
||||||
|
class UserEngagementEmailService {
|
||||||
|
constructor() {
|
||||||
|
this.emailClient = new EmailClient();
|
||||||
|
this.templateManager = new TemplateManager();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the user engagement email service
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.emailClient.initialize(),
|
||||||
|
this.templateManager.initialize(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
console.log("User Engagement Email Service initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send first listing celebration email to owner
|
||||||
|
* @param {Object} owner - Owner user object
|
||||||
|
* @param {string} owner.firstName - Owner's first name
|
||||||
|
* @param {string} owner.email - Owner's email address
|
||||||
|
* @param {Object} item - Item object
|
||||||
|
* @param {number} item.id - Item ID
|
||||||
|
* @param {string} item.name - Item name
|
||||||
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||||
|
*/
|
||||||
|
async sendFirstListingCelebrationEmail(owner, item) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
ownerName: owner.firstName || "there",
|
||||||
|
itemName: item.name,
|
||||||
|
itemId: item.id,
|
||||||
|
viewItemUrl: `${frontendUrl}/items/${item.id}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlContent = await this.templateManager.renderTemplate(
|
||||||
|
"firstListingCelebrationToOwner",
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
const subject = `Congratulations! Your first item is live on RentAll`;
|
||||||
|
|
||||||
|
return await this.emailClient.sendEmail(
|
||||||
|
owner.email,
|
||||||
|
subject,
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send first listing celebration email:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UserEngagementEmailService;
|
||||||
58
backend/services/email/index.js
Normal file
58
backend/services/email/index.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const AuthEmailService = require("./domain/AuthEmailService");
|
||||||
|
const FeedbackEmailService = require("./domain/FeedbackEmailService");
|
||||||
|
const ForumEmailService = require("./domain/ForumEmailService");
|
||||||
|
const MessagingEmailService = require("./domain/MessagingEmailService");
|
||||||
|
const CustomerServiceEmailService = require("./domain/CustomerServiceEmailService");
|
||||||
|
const RentalFlowEmailService = require("./domain/RentalFlowEmailService");
|
||||||
|
const RentalReminderEmailService = require("./domain/RentalReminderEmailService");
|
||||||
|
const UserEngagementEmailService = require("./domain/UserEngagementEmailService");
|
||||||
|
const AlphaInvitationEmailService = require("./domain/AlphaInvitationEmailService");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EmailServices aggregates all domain-specific email services
|
||||||
|
* This class provides a unified interface to access all email functionality
|
||||||
|
*/
|
||||||
|
class EmailServices {
|
||||||
|
constructor() {
|
||||||
|
// Initialize all domain services
|
||||||
|
this.auth = new AuthEmailService();
|
||||||
|
this.feedback = new FeedbackEmailService();
|
||||||
|
this.forum = new ForumEmailService();
|
||||||
|
this.messaging = new MessagingEmailService();
|
||||||
|
this.customerService = new CustomerServiceEmailService();
|
||||||
|
this.rentalFlow = new RentalFlowEmailService();
|
||||||
|
this.rentalReminder = new RentalReminderEmailService();
|
||||||
|
this.userEngagement = new UserEngagementEmailService();
|
||||||
|
this.alphaInvitation = new AlphaInvitationEmailService();
|
||||||
|
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all email services
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.auth.initialize(),
|
||||||
|
this.feedback.initialize(),
|
||||||
|
this.forum.initialize(),
|
||||||
|
this.messaging.initialize(),
|
||||||
|
this.customerService.initialize(),
|
||||||
|
this.rentalFlow.initialize(),
|
||||||
|
this.rentalReminder.initialize(),
|
||||||
|
this.userEngagement.initialize(),
|
||||||
|
this.alphaInvitation.initialize(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
console.log("All Email Services initialized successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export singleton instance
|
||||||
|
const emailServices = new EmailServices();
|
||||||
|
|
||||||
|
module.exports = emailServices;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
const { Rental, Item } = require("../models");
|
const { Rental, Item, User } = require("../models");
|
||||||
const emailService = require("./emailService");
|
const emailServices = require("./email");
|
||||||
|
|
||||||
class LateReturnService {
|
class LateReturnService {
|
||||||
/**
|
/**
|
||||||
@@ -91,8 +91,14 @@ class LateReturnService {
|
|||||||
|
|
||||||
// Send notification to customer service if late return detected
|
// Send notification to customer service if late return detected
|
||||||
if (lateCalculation.isLate && lateCalculation.lateFee > 0) {
|
if (lateCalculation.isLate && lateCalculation.lateFee > 0) {
|
||||||
await emailService.sendLateReturnToCustomerService(
|
// Fetch owner and renter user data for email
|
||||||
|
const owner = await User.findByPk(updatedRental.ownerId);
|
||||||
|
const renter = await User.findByPk(updatedRental.renterId);
|
||||||
|
|
||||||
|
await emailServices.customerService.sendLateReturnToCustomerService(
|
||||||
updatedRental,
|
updatedRental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
lateCalculation
|
lateCalculation
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const { Rental, User, Item } = require("../models");
|
const { Rental, User, Item } = require("../models");
|
||||||
const StripeService = require("./stripeService");
|
const StripeService = require("./stripeService");
|
||||||
const emailService = require("./emailService");
|
const emailServices = require("./email");
|
||||||
const { Op } = require("sequelize");
|
const { Op } = require("sequelize");
|
||||||
|
|
||||||
class PayoutService {
|
class PayoutService {
|
||||||
@@ -82,7 +82,7 @@ class PayoutService {
|
|||||||
|
|
||||||
// Send payout notification email to owner
|
// Send payout notification email to owner
|
||||||
try {
|
try {
|
||||||
await emailService.sendPayoutReceivedEmail(rental);
|
await emailServices.rentalFlow.sendPayoutReceivedEmail(rental.owner, rental);
|
||||||
console.log(
|
console.log(
|
||||||
`Payout notification email sent to owner for rental ${rental.id}`
|
`Payout notification email sent to owner for rental ${rental.id}`
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user