email refactor

This commit is contained in:
jackiettran
2025-11-14 17:36:35 -05:00
parent 629f0055a1
commit 3a6da3d47d
25 changed files with 3176 additions and 2219 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
); );

View File

@@ -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);

View File

@@ -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:

View File

@@ -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}`);

View File

@@ -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
); );

View 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;

View 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;

View 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(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/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,
};

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

File diff suppressed because it is too large Load Diff

View 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;

View 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;

View 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

View File

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

View File

@@ -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}`
); );