email refactor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
const { Rental, Item, ConditionCheck } = require("../models");
|
||||
const { Rental, Item, ConditionCheck, User } = require("../models");
|
||||
const LateReturnService = require("./lateReturnService");
|
||||
const emailService = require("./emailService");
|
||||
const emailServices = require("./email");
|
||||
|
||||
class DamageAssessmentService {
|
||||
/**
|
||||
@@ -119,9 +119,15 @@ class DamageAssessmentService {
|
||||
|
||||
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
|
||||
await emailService.sendDamageReportToCustomerService(
|
||||
await emailServices.customerService.sendDamageReportToCustomerService(
|
||||
updatedRental,
|
||||
owner,
|
||||
renter,
|
||||
damageAssessment,
|
||||
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 emailService = require("./emailService");
|
||||
const { Rental, Item, User } = require("../models");
|
||||
const emailServices = require("./email");
|
||||
|
||||
class LateReturnService {
|
||||
/**
|
||||
@@ -91,8 +91,14 @@ class LateReturnService {
|
||||
|
||||
// Send notification to customer service if late return detected
|
||||
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,
|
||||
owner,
|
||||
renter,
|
||||
lateCalculation
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { Rental, User, Item } = require("../models");
|
||||
const StripeService = require("./stripeService");
|
||||
const emailService = require("./emailService");
|
||||
const emailServices = require("./email");
|
||||
const { Op } = require("sequelize");
|
||||
|
||||
class PayoutService {
|
||||
@@ -82,7 +82,7 @@ class PayoutService {
|
||||
|
||||
// Send payout notification email to owner
|
||||
try {
|
||||
await emailService.sendPayoutReceivedEmail(rental);
|
||||
await emailServices.rentalFlow.sendPayoutReceivedEmail(rental.owner, rental);
|
||||
console.log(
|
||||
`Payout notification email sent to owner for rental ${rental.id}`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user