1855 lines
65 KiB
JavaScript
1855 lines
65 KiB
JavaScript
const { SESClient, SendEmailCommand } = require("@aws-sdk/client-ses");
|
|
const fs = require("fs").promises;
|
|
const path = require("path");
|
|
const { getAWSConfig } = require("../config/aws");
|
|
const { User } = require("../models");
|
|
|
|
class EmailService {
|
|
constructor() {
|
|
this.sesClient = null;
|
|
this.initialized = false;
|
|
this.templates = new Map();
|
|
}
|
|
|
|
async initialize() {
|
|
if (this.initialized) return;
|
|
|
|
try {
|
|
// Use centralized AWS configuration with credential profiles
|
|
const awsConfig = getAWSConfig();
|
|
this.sesClient = new SESClient(awsConfig);
|
|
|
|
await this.loadEmailTemplates();
|
|
this.initialized = true;
|
|
console.log("SES Email Service initialized successfully");
|
|
} catch (error) {
|
|
console.error("Failed to initialize SES Email Service:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
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",
|
|
];
|
|
|
|
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.warn("Templates directory not found, using fallback templates");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert HTML to plain text for email fallback
|
|
* Strips HTML tags and formats content for plain text email clients
|
|
*/
|
|
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()
|
|
);
|
|
}
|
|
|
|
async sendEmail(to, subject, htmlContent, textContent = null) {
|
|
if (!this.initialized) {
|
|
await this.initialize();
|
|
}
|
|
|
|
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 = this.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",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
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 };
|
|
}
|
|
}
|
|
|
|
renderTemplate(templateName, variables = {}) {
|
|
let template = this.templates.get(templateName);
|
|
|
|
if (!template) {
|
|
template = this.getFallbackTemplate(templateName);
|
|
}
|
|
|
|
let rendered = template;
|
|
|
|
Object.keys(variables).forEach((key) => {
|
|
const regex = new RegExp(`{{${key}}}`, "g");
|
|
rendered = rendered.replace(regex, variables[key] || "");
|
|
});
|
|
|
|
return rendered;
|
|
}
|
|
|
|
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>Platform Fee (20%):</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="{{myListingsUrl}}" 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>
|
|
`
|
|
)
|
|
);
|
|
}
|
|
|
|
async sendConditionCheckReminder(userEmail, notification, rental) {
|
|
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 = this.renderTemplate(
|
|
"conditionCheckReminderToUser",
|
|
variables
|
|
);
|
|
|
|
return await this.sendEmail(
|
|
userEmail,
|
|
`RentAll: ${notification.title}`,
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
async sendRentalConfirmation(
|
|
userEmail,
|
|
notification,
|
|
rental,
|
|
recipientName = null,
|
|
isRenter = false
|
|
) {
|
|
const itemName = rental?.item?.name || "Unknown Item";
|
|
|
|
const variables = {
|
|
recipientName: recipientName || "there",
|
|
title: notification.title,
|
|
message: notification.message,
|
|
itemName: itemName,
|
|
startDate: rental?.startDateTime
|
|
? new Date(rental.startDateTime).toLocaleDateString()
|
|
: "Not specified",
|
|
endDate: rental?.endDateTime
|
|
? new Date(rental.endDateTime).toLocaleDateString()
|
|
: "Not specified",
|
|
isRenter: isRenter,
|
|
};
|
|
|
|
// Add payment information if this is for the renter and rental has payment info
|
|
let paymentSection = "";
|
|
if (isRenter) {
|
|
const totalAmount = parseFloat(rental.totalAmount) || 0;
|
|
const isPaidRental = totalAmount > 0 && rental.paymentStatus === 'paid';
|
|
|
|
if (isPaidRental) {
|
|
// Format payment method display
|
|
let paymentMethodDisplay = "Payment method on file";
|
|
if (rental.paymentMethodBrand && rental.paymentMethodLast4) {
|
|
const brandCapitalized = rental.paymentMethodBrand.charAt(0).toUpperCase() +
|
|
rental.paymentMethodBrand.slice(1);
|
|
paymentMethodDisplay = `${brandCapitalized} ending in ${rental.paymentMethodLast4}`;
|
|
}
|
|
|
|
const chargedAtFormatted = rental.chargedAt
|
|
? new Date(rental.chargedAt).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short"
|
|
})
|
|
: new Date().toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short"
|
|
});
|
|
|
|
// Build payment receipt section HTML
|
|
paymentSection = `
|
|
<h2>Payment Receipt</h2>
|
|
<div class="success-box">
|
|
<div class="icon">💳</div>
|
|
<p><strong>Payment Successful</strong></p>
|
|
<p>Your payment has been processed. This email serves as your receipt.</p>
|
|
</div>
|
|
<table class="info-table">
|
|
<tr>
|
|
<th>Amount Charged</th>
|
|
<td><strong>$${totalAmount.toFixed(2)}</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<th>Payment Method</th>
|
|
<td>${paymentMethodDisplay}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Transaction ID</th>
|
|
<td style="font-family: monospace; font-size: 12px;">${rental.stripePaymentIntentId || "N/A"}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Transaction Date</th>
|
|
<td>${chargedAtFormatted}</td>
|
|
</tr>
|
|
</table>
|
|
<p style="font-size: 13px; color: #6c757d; margin-top: 10px;">
|
|
<strong>Note:</strong> Keep this email for your records. You can use the transaction ID above if you need to contact support about this payment.
|
|
</p>
|
|
`;
|
|
} else if (totalAmount === 0) {
|
|
// Free rental message
|
|
paymentSection = `
|
|
<div class="success-box">
|
|
<p><strong>No Payment Required:</strong> This is a free rental.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
variables.paymentSection = paymentSection;
|
|
|
|
const htmlContent = this.renderTemplate("rentalConfirmationToUser", variables);
|
|
|
|
// Use clear, transactional subject line with item name
|
|
const subject = `Rental Confirmation - ${itemName}`;
|
|
|
|
return await this.sendEmail(userEmail, subject, htmlContent);
|
|
}
|
|
|
|
async sendVerificationEmail(user, verificationToken) {
|
|
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 = this.renderTemplate("emailVerificationToUser", variables);
|
|
|
|
return await this.sendEmail(
|
|
user.email,
|
|
"Verify Your Email - RentAll",
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
async sendAlphaInvitation(email, code) {
|
|
// Ensure service is initialized before rendering template
|
|
if (!this.initialized) {
|
|
await this.initialize();
|
|
}
|
|
|
|
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 = this.renderTemplate("alphaInvitationToUser", variables);
|
|
|
|
return await this.sendEmail(
|
|
email,
|
|
"Your Alpha Access Code - RentAll",
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
async sendPasswordResetEmail(user, resetToken) {
|
|
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 = this.renderTemplate("passwordResetToUser", variables);
|
|
|
|
return await this.sendEmail(
|
|
user.email,
|
|
"Reset Your Password - RentAll",
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
async sendPasswordChangedEmail(user) {
|
|
const timestamp = new Date().toLocaleString("en-US", {
|
|
dateStyle: "long",
|
|
timeStyle: "short",
|
|
});
|
|
|
|
const variables = {
|
|
recipientName: user.firstName || "there",
|
|
email: user.email,
|
|
timestamp: timestamp,
|
|
};
|
|
|
|
const htmlContent = this.renderTemplate("passwordChangedToUser", variables);
|
|
|
|
return await this.sendEmail(
|
|
user.email,
|
|
"Password Changed Successfully - RentAll",
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
async sendRentalRequestEmail(rental) {
|
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
|
const approveUrl = `${frontendUrl}/my-listings?rentalId=${rental.id}`;
|
|
|
|
// Fetch owner details
|
|
const owner = await User.findByPk(rental.ownerId, {
|
|
attributes: ["email", "firstName", "lastName"],
|
|
});
|
|
|
|
// Fetch renter details
|
|
const renter = await User.findByPk(rental.renterId, {
|
|
attributes: ["firstName", "lastName"],
|
|
});
|
|
|
|
if (!owner || !renter) {
|
|
console.error(
|
|
"Owner or renter not found for rental request notification"
|
|
);
|
|
return { success: false, error: "User not found" };
|
|
}
|
|
|
|
const variables = {
|
|
ownerName: owner.firstName,
|
|
renterName: `${renter.firstName} ${renter.lastName}`.trim() || "A renter",
|
|
itemName: rental.item?.name || "your item",
|
|
startDate: rental.startDateTime
|
|
? new Date(rental.startDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified",
|
|
endDate: rental.endDateTime
|
|
? new Date(rental.endDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified",
|
|
totalAmount: rental.totalAmount
|
|
? parseFloat(rental.totalAmount).toFixed(2)
|
|
: "0.00",
|
|
payoutAmount: rental.payoutAmount
|
|
? parseFloat(rental.payoutAmount).toFixed(2)
|
|
: "0.00",
|
|
deliveryMethod: rental.deliveryMethod || "Not specified",
|
|
rentalNotes: rental.notes || "No additional notes provided",
|
|
approveUrl: approveUrl,
|
|
};
|
|
|
|
const htmlContent = this.renderTemplate("rentalRequestToOwner", variables);
|
|
|
|
return await this.sendEmail(
|
|
owner.email,
|
|
`Rental Request for ${rental.item?.name || "Your Item"}`,
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
async sendRentalRequestConfirmationEmail(rental) {
|
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
|
const viewRentalsUrl = `${frontendUrl}/my-rentals`;
|
|
|
|
// Fetch renter details
|
|
const renter = await User.findByPk(rental.renterId, {
|
|
attributes: ["email", "firstName", "lastName"],
|
|
});
|
|
|
|
if (!renter) {
|
|
console.error(
|
|
"Renter not found for rental request confirmation notification"
|
|
);
|
|
return { success: false, error: "Renter not found" };
|
|
}
|
|
|
|
// Determine payment message based on rental amount
|
|
const totalAmount = parseFloat(rental.totalAmount) || 0;
|
|
const paymentMessage =
|
|
totalAmount > 0
|
|
? "The owner will review your request. You'll only be charged if they approve it."
|
|
: "The owner will review your request and respond soon.";
|
|
|
|
const variables = {
|
|
renterName: renter.firstName || "there",
|
|
itemName: rental.item?.name || "the item",
|
|
startDate: rental.startDateTime
|
|
? new Date(rental.startDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified",
|
|
endDate: rental.endDateTime
|
|
? new Date(rental.endDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified",
|
|
totalAmount: totalAmount.toFixed(2),
|
|
deliveryMethod: rental.deliveryMethod || "Not specified",
|
|
paymentMessage: paymentMessage,
|
|
viewRentalsUrl: viewRentalsUrl,
|
|
};
|
|
|
|
const htmlContent = this.renderTemplate(
|
|
"rentalRequestConfirmationToRenter",
|
|
variables
|
|
);
|
|
|
|
return await this.sendEmail(
|
|
renter.email,
|
|
`Rental Request Submitted - ${rental.item?.name || "Item"}`,
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
async sendRentalDeclinedEmail(rental, declineReason) {
|
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
|
const browseItemsUrl = `${frontendUrl}/`;
|
|
|
|
// Fetch renter details
|
|
const renter = await User.findByPk(rental.renterId, {
|
|
attributes: ["email", "firstName", "lastName"],
|
|
});
|
|
|
|
if (!renter) {
|
|
console.error(
|
|
"Renter not found for rental decline notification"
|
|
);
|
|
return { success: false, error: "Renter not found" };
|
|
}
|
|
|
|
// Determine payment message based on rental amount
|
|
const totalAmount = parseFloat(rental.totalAmount) || 0;
|
|
const paymentMessage =
|
|
totalAmount > 0
|
|
? "Since your request was declined before payment was processed, you will not be charged."
|
|
: "No payment was required for this rental request.";
|
|
|
|
// Build owner message section if decline reason provided
|
|
const ownerMessage = declineReason
|
|
? `
|
|
<div class="notice-box">
|
|
<p><strong>Message from the owner:</strong></p>
|
|
<p>${declineReason}</p>
|
|
</div>
|
|
`
|
|
: "";
|
|
|
|
const variables = {
|
|
renterName: renter.firstName || "there",
|
|
itemName: rental.item?.name || "the item",
|
|
startDate: rental.startDateTime
|
|
? new Date(rental.startDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified",
|
|
endDate: rental.endDateTime
|
|
? new Date(rental.endDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified",
|
|
deliveryMethod: rental.deliveryMethod || "Not specified",
|
|
paymentMessage: paymentMessage,
|
|
ownerMessage: ownerMessage,
|
|
browseItemsUrl: browseItemsUrl,
|
|
payoutAmount: rental.payoutAmount
|
|
? parseFloat(rental.payoutAmount).toFixed(2)
|
|
: "0.00",
|
|
totalAmount: totalAmount.toFixed(2),
|
|
};
|
|
|
|
const htmlContent = this.renderTemplate(
|
|
"rentalDeclinedToRenter",
|
|
variables
|
|
);
|
|
|
|
return await this.sendEmail(
|
|
renter.email,
|
|
`Rental Request Declined - ${rental.item?.name || "Item"}`,
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
async sendPayoutReceivedEmail(rental) {
|
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
|
const earningsDashboardUrl = `${frontendUrl}/earnings`;
|
|
|
|
// Fetch owner details
|
|
const owner = await User.findByPk(rental.ownerId, {
|
|
attributes: ["email", "firstName", "lastName"],
|
|
});
|
|
|
|
if (!owner) {
|
|
console.error("Owner not found for payout notification");
|
|
return { success: false, error: "Owner not found" };
|
|
}
|
|
|
|
// Format currency values
|
|
const totalAmount = parseFloat(rental.totalAmount) || 0;
|
|
const platformFee = parseFloat(rental.platformFee) || 0;
|
|
const payoutAmount = parseFloat(rental.payoutAmount) || 0;
|
|
|
|
const variables = {
|
|
ownerName: owner.firstName || "there",
|
|
itemName: rental.item?.name || "your item",
|
|
startDate: rental.startDateTime
|
|
? new Date(rental.startDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified",
|
|
endDate: rental.endDateTime
|
|
? new Date(rental.endDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified",
|
|
totalAmount: totalAmount.toFixed(2),
|
|
platformFee: platformFee.toFixed(2),
|
|
payoutAmount: payoutAmount.toFixed(2),
|
|
stripeTransferId: rental.stripeTransferId || "N/A",
|
|
earningsDashboardUrl: earningsDashboardUrl,
|
|
};
|
|
|
|
const htmlContent = this.renderTemplate("payoutReceivedToOwner", variables);
|
|
|
|
return await this.sendEmail(
|
|
owner.email,
|
|
`Earnings Received - $${payoutAmount.toFixed(2)} for ${rental.item?.name || "Your Item"}`,
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
async sendRentalCancellationEmails(rental, refundInfo) {
|
|
const results = {
|
|
confirmationEmailSent: false,
|
|
notificationEmailSent: false,
|
|
};
|
|
|
|
try {
|
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
|
const browseUrl = `${frontendUrl}/`;
|
|
|
|
// Fetch both owner and renter details
|
|
const owner = await User.findByPk(rental.ownerId, {
|
|
attributes: ["email", "firstName", "lastName"],
|
|
});
|
|
const renter = await User.findByPk(rental.renterId, {
|
|
attributes: ["email", "firstName", "lastName"],
|
|
});
|
|
|
|
if (!owner || !renter) {
|
|
console.error(
|
|
"Owner or renter not found for rental cancellation emails"
|
|
);
|
|
return { success: false, error: "User not found" };
|
|
}
|
|
|
|
const cancelledBy = rental.cancelledBy; // 'owner' or 'renter'
|
|
const itemName = rental.item?.name || "the item";
|
|
const startDate = rental.startDateTime
|
|
? new Date(rental.startDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified";
|
|
const endDate = rental.endDateTime
|
|
? new Date(rental.endDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified";
|
|
const cancelledAt = rental.cancelledAt
|
|
? new Date(rental.cancelledAt).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified";
|
|
|
|
// Determine who gets confirmation and who gets notification
|
|
let confirmationRecipient, notificationRecipient;
|
|
let confirmationRecipientName, notificationRecipientName;
|
|
let cancellationMessage, additionalInfo;
|
|
|
|
if (cancelledBy === "owner") {
|
|
// Owner cancelled: owner gets confirmation, renter gets notification
|
|
confirmationRecipient = owner.email;
|
|
confirmationRecipientName = owner.firstName || "there";
|
|
notificationRecipient = renter.email;
|
|
notificationRecipientName = renter.firstName || "there";
|
|
|
|
cancellationMessage = `The owner has cancelled the rental for <strong>${itemName}</strong>. We apologize for any inconvenience this may cause.`;
|
|
|
|
// Only show refund info if rental had a cost
|
|
if (rental.totalAmount > 0) {
|
|
additionalInfo = `
|
|
<div class="info-box">
|
|
<p><strong>Full Refund Processed</strong></p>
|
|
<p>You will receive a full refund of $${refundInfo.amount.toFixed(2)}. The refund will appear in your account within 5-10 business days.</p>
|
|
</div>
|
|
<div style="text-align: center">
|
|
<a href="${browseUrl}" class="button">Browse Other Items</a>
|
|
</div>
|
|
`;
|
|
} else {
|
|
additionalInfo = `
|
|
<div class="info-box">
|
|
<p>This rental has been cancelled by the owner. We apologize for any inconvenience.</p>
|
|
</div>
|
|
<div style="text-align: center">
|
|
<a href="${browseUrl}" class="button">Browse Other Items</a>
|
|
</div>
|
|
`;
|
|
}
|
|
} else {
|
|
// Renter cancelled: renter gets confirmation, owner gets notification
|
|
confirmationRecipient = renter.email;
|
|
confirmationRecipientName = renter.firstName || "there";
|
|
notificationRecipient = owner.email;
|
|
notificationRecipientName = owner.firstName || "there";
|
|
|
|
cancellationMessage = `The renter has cancelled their rental for <strong>${itemName}</strong>.`;
|
|
additionalInfo = `
|
|
<div class="info-box">
|
|
<p><strong>Your item is now available</strong></p>
|
|
<p>Your item is now available for other renters to book for these dates.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Build refund section for confirmation email (only for paid rentals)
|
|
let refundSection = "";
|
|
if (rental.totalAmount > 0) {
|
|
if (refundInfo.amount > 0) {
|
|
const refundPercentage = (refundInfo.percentage * 100).toFixed(0);
|
|
refundSection = `
|
|
<h2>Refund Information</h2>
|
|
<div class="refund-amount">$${refundInfo.amount.toFixed(2)}</div>
|
|
<div class="info-box">
|
|
<p><strong>Refund Amount:</strong> $${refundInfo.amount.toFixed(2)} (${refundPercentage}% of total)</p>
|
|
<p><strong>Reason:</strong> ${refundInfo.reason}</p>
|
|
<p><strong>Processing Time:</strong> Refunds typically appear within 5-10 business days.</p>
|
|
</div>
|
|
`;
|
|
} else {
|
|
refundSection = `
|
|
<h2>Refund Information</h2>
|
|
<div class="warning-box">
|
|
<p><strong>No Refund Available</strong></p>
|
|
<p>${refundInfo.reason}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
// For free rentals (totalAmount = 0), refundSection stays empty
|
|
|
|
// Send confirmation email to canceller
|
|
try {
|
|
const confirmationVariables = {
|
|
recipientName: confirmationRecipientName,
|
|
itemName: itemName,
|
|
startDate: startDate,
|
|
endDate: endDate,
|
|
cancelledAt: cancelledAt,
|
|
refundSection: refundSection,
|
|
};
|
|
|
|
const confirmationHtml = this.renderTemplate(
|
|
"rentalCancellationConfirmationToUser",
|
|
confirmationVariables
|
|
);
|
|
|
|
const confirmationResult = await this.sendEmail(
|
|
confirmationRecipient,
|
|
`Cancellation Confirmed - ${itemName}`,
|
|
confirmationHtml
|
|
);
|
|
|
|
if (confirmationResult.success) {
|
|
console.log(
|
|
`Cancellation confirmation email sent to ${cancelledBy}: ${confirmationRecipient}`
|
|
);
|
|
results.confirmationEmailSent = true;
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
`Failed to send cancellation confirmation email to ${cancelledBy}:`,
|
|
error.message
|
|
);
|
|
}
|
|
|
|
// Send notification email to other party
|
|
try {
|
|
const notificationVariables = {
|
|
recipientName: notificationRecipientName,
|
|
itemName: itemName,
|
|
startDate: startDate,
|
|
endDate: endDate,
|
|
cancelledAt: cancelledAt,
|
|
cancellationMessage: cancellationMessage,
|
|
additionalInfo: additionalInfo,
|
|
};
|
|
|
|
const notificationHtml = this.renderTemplate(
|
|
"rentalCancellationNotificationToUser",
|
|
notificationVariables
|
|
);
|
|
|
|
const notificationResult = await this.sendEmail(
|
|
notificationRecipient,
|
|
`Rental Cancelled - ${itemName}`,
|
|
notificationHtml
|
|
);
|
|
|
|
if (notificationResult.success) {
|
|
console.log(
|
|
`Cancellation notification email sent to ${cancelledBy === "owner" ? "renter" : "owner"}: ${notificationRecipient}`
|
|
);
|
|
results.notificationEmailSent = true;
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
`Failed to send cancellation notification email:`,
|
|
error.message
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error sending cancellation emails:", error);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
async sendTemplateEmail(toEmail, subject, templateName, variables = {}) {
|
|
const htmlContent = this.renderTemplate(templateName, variables);
|
|
return await this.sendEmail(toEmail, subject, htmlContent);
|
|
}
|
|
|
|
async sendLateReturnToCustomerService(rental, lateCalculation) {
|
|
try {
|
|
// Get owner and renter details
|
|
const owner = await User.findByPk(rental.ownerId);
|
|
const renter = await User.findByPk(rental.renterId);
|
|
|
|
if (!owner || !renter) {
|
|
console.error("Owner or renter not found for late return notification");
|
|
return;
|
|
}
|
|
|
|
// Format dates
|
|
const scheduledEnd = new Date(rental.endDateTime).toLocaleString();
|
|
const actualReturn = new Date(
|
|
rental.actualReturnDateTime
|
|
).toLocaleString();
|
|
|
|
// Send email to customer service
|
|
await this.sendTemplateEmail(
|
|
process.env.CUSTOMER_SUPPORT_EMAIL,
|
|
"Late Return Detected - Action Required",
|
|
"lateReturnToCS",
|
|
{
|
|
rentalId: rental.id,
|
|
itemName: rental.item.name,
|
|
ownerName: owner.name,
|
|
ownerEmail: owner.email,
|
|
renterName: renter.name,
|
|
renterEmail: renter.email,
|
|
scheduledEnd,
|
|
actualReturn,
|
|
hoursLate: lateCalculation.lateHours.toFixed(1),
|
|
lateFee: lateCalculation.lateFee.toFixed(2),
|
|
}
|
|
);
|
|
|
|
console.log(
|
|
`Late return notification sent to customer service for rental ${rental.id}`
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
"Failed to send late return notification to customer service:",
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
async sendDamageReportToCustomerService(
|
|
rental,
|
|
damageAssessment,
|
|
lateCalculation = null
|
|
) {
|
|
try {
|
|
// Get owner and renter details
|
|
const owner = await User.findByPk(rental.ownerId);
|
|
const renter = await User.findByPk(rental.renterId);
|
|
|
|
if (!owner || !renter) {
|
|
console.error(
|
|
"Owner or renter not found for damage report notification"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 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";
|
|
}
|
|
|
|
// Send email to customer service
|
|
await this.sendTemplateEmail(
|
|
process.env.CUSTOMER_SUPPORT_EMAIL,
|
|
"Damage Report Filed - Action Required",
|
|
"damageReportToCS",
|
|
{
|
|
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",
|
|
}
|
|
);
|
|
|
|
console.log(
|
|
`Damage report notification sent to customer service for rental ${rental.id}`
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
"Failed to send damage report notification to customer service:",
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
async sendLostItemToCustomerService(rental) {
|
|
try {
|
|
// Get owner and renter details
|
|
const owner = await User.findByPk(rental.ownerId);
|
|
const renter = await User.findByPk(rental.renterId);
|
|
|
|
if (!owner || !renter) {
|
|
console.error("Owner or renter not found for lost item notification");
|
|
return;
|
|
}
|
|
|
|
// Format dates
|
|
const reportedAt = new Date(rental.itemLostReportedAt).toLocaleString();
|
|
const scheduledReturnDate = new Date(rental.endDateTime).toLocaleString();
|
|
|
|
// Send email to customer service
|
|
await this.sendTemplateEmail(
|
|
process.env.CUSTOMER_SUPPORT_EMAIL,
|
|
"Lost Item Claim Filed - Action Required",
|
|
"lostItemToCS",
|
|
{
|
|
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),
|
|
}
|
|
);
|
|
|
|
console.log(
|
|
`Lost item notification sent to customer service for rental ${rental.id}`
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
"Failed to send lost item notification to customer service:",
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
async sendRentalConfirmationEmails(rental) {
|
|
const results = {
|
|
ownerEmailSent: false,
|
|
renterEmailSent: false,
|
|
};
|
|
|
|
try {
|
|
// Get owner and renter details
|
|
const owner = await User.findByPk(rental.ownerId, {
|
|
attributes: ["email", "firstName"],
|
|
});
|
|
const renter = await User.findByPk(rental.renterId, {
|
|
attributes: ["email", "firstName"],
|
|
});
|
|
|
|
// Create notification data for owner
|
|
const ownerNotification = {
|
|
type: "rental_confirmed",
|
|
title: "Rental Confirmed",
|
|
message: `Your "${rental.item.name}" has been confirmed for rental.`,
|
|
rentalId: rental.id,
|
|
userId: rental.ownerId,
|
|
metadata: { rentalStart: rental.startDateTime },
|
|
};
|
|
|
|
// Create notification data for renter
|
|
const renterNotification = {
|
|
type: "rental_confirmed",
|
|
title: "Rental Confirmed",
|
|
message: `Your rental of "${rental.item.name}" has been confirmed.`,
|
|
rentalId: rental.id,
|
|
userId: rental.renterId,
|
|
metadata: { rentalStart: rental.startDateTime },
|
|
};
|
|
|
|
// Send email to owner - independent error handling
|
|
if (owner?.email) {
|
|
try {
|
|
const ownerResult = await this.sendRentalConfirmation(
|
|
owner.email,
|
|
ownerNotification,
|
|
rental,
|
|
owner.firstName,
|
|
false // isRenter = false for owner
|
|
);
|
|
if (ownerResult.success) {
|
|
console.log(
|
|
`Rental confirmation email sent to owner: ${owner.email}`
|
|
);
|
|
results.ownerEmailSent = true;
|
|
} else {
|
|
console.error(
|
|
`Failed to send rental confirmation email to owner (${owner.email}):`,
|
|
ownerResult.error
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
`Failed to send rental confirmation email to owner (${owner.email}):`,
|
|
error.message
|
|
);
|
|
}
|
|
}
|
|
|
|
// Send email to renter - independent error handling
|
|
if (renter?.email) {
|
|
try {
|
|
const renterResult = await this.sendRentalConfirmation(
|
|
renter.email,
|
|
renterNotification,
|
|
rental,
|
|
renter.firstName,
|
|
true // isRenter = true for renter (enables payment receipt)
|
|
);
|
|
if (renterResult.success) {
|
|
console.log(
|
|
`Rental confirmation email sent to renter: ${renter.email}`
|
|
);
|
|
results.renterEmailSent = true;
|
|
} else {
|
|
console.error(
|
|
`Failed to send rental confirmation email to renter (${renter.email}):`,
|
|
renterResult.error
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
`Failed to send rental confirmation email to renter (${renter.email}):`,
|
|
error.message
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
"Error fetching user data for rental confirmation emails:",
|
|
error
|
|
);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
async sendFirstListingCelebrationEmail(owner, item) {
|
|
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 = this.renderTemplate(
|
|
"firstListingCelebrationToOwner",
|
|
variables
|
|
);
|
|
|
|
const subject = `🎉 Congratulations! Your first item is live on RentAll`;
|
|
|
|
return await this.sendEmail(owner.email, subject, htmlContent);
|
|
}
|
|
|
|
async sendRentalApprovalConfirmationEmail(rental) {
|
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
|
|
|
// Fetch owner details
|
|
const owner = await User.findByPk(rental.ownerId, {
|
|
attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"],
|
|
});
|
|
|
|
// Fetch renter details
|
|
const renter = await User.findByPk(rental.renterId, {
|
|
attributes: ["firstName", "lastName"],
|
|
});
|
|
|
|
if (!owner || !renter) {
|
|
console.error(
|
|
"Owner or renter not found for rental approval confirmation email"
|
|
);
|
|
return { success: false, error: "User not found" };
|
|
}
|
|
|
|
// Determine if Stripe setup is needed
|
|
const hasStripeAccount = !!owner.stripeConnectedAccountId;
|
|
const totalAmount = parseFloat(rental.totalAmount) || 0;
|
|
const payoutAmount = parseFloat(rental.payoutAmount) || 0;
|
|
const platformFee = parseFloat(rental.platformFee) || 0;
|
|
|
|
// Build payment message
|
|
const isPaidRental = totalAmount > 0;
|
|
let paymentMessage = "";
|
|
if (isPaidRental) {
|
|
paymentMessage = "their payment has been processed successfully.";
|
|
} else {
|
|
paymentMessage = "this is a free rental (no payment required).";
|
|
}
|
|
|
|
// Build earnings section (only for paid rentals)
|
|
let earningsSection = "";
|
|
if (isPaidRental) {
|
|
earningsSection = `
|
|
<h2>Your Earnings</h2>
|
|
<table class="info-table">
|
|
<tr>
|
|
<th>Total Rental Amount</th>
|
|
<td>\$${totalAmount.toFixed(2)}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Platform Fee (20%)</th>
|
|
<td>-\$${platformFee.toFixed(2)}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Your Payout</th>
|
|
<td class="highlight">\$${payoutAmount.toFixed(2)}</td>
|
|
</tr>
|
|
</table>
|
|
`;
|
|
}
|
|
|
|
// Build conditional Stripe section based on Stripe status
|
|
let stripeSection = "";
|
|
if (!hasStripeAccount && isPaidRental) {
|
|
// Only show Stripe setup reminder for paid rentals
|
|
stripeSection = `
|
|
<div class="warning-box">
|
|
<p><strong>⚠️ Action Required: Set Up Your Earnings Account</strong></p>
|
|
<p>To receive your payout of <strong>\$${payoutAmount.toFixed(2)}</strong> when this rental completes, you need to set up your earnings account.</p>
|
|
</div>
|
|
<h2>Set Up Earnings to Get Paid</h2>
|
|
<div class="info-box">
|
|
<p><strong>Why set up now?</strong></p>
|
|
<ul>
|
|
<li><strong>Automatic payouts</strong> when rentals complete</li>
|
|
<li><strong>Secure transfers</strong> directly to your bank account</li>
|
|
<li><strong>Track all earnings</strong> in one dashboard</li>
|
|
<li><strong>Fast deposits</strong> (typically 2-3 business days)</li>
|
|
</ul>
|
|
<p>Setup only takes about 5 minutes and you only need to do it once.</p>
|
|
</div>
|
|
<p style="text-align: center;">
|
|
<a href="${frontendUrl}/earnings" class="button">Set Up Earnings Account Now</a>
|
|
</p>
|
|
<p style="text-align: center; font-size: 14px; color: #856404;">
|
|
<strong>Important:</strong> Without earnings setup, you won't receive payouts automatically when rentals complete.
|
|
</p>
|
|
`;
|
|
} else if (hasStripeAccount && isPaidRental) {
|
|
stripeSection = `
|
|
<div class="success-box">
|
|
<p><strong>✓ Earnings Account Active</strong></p>
|
|
<p>Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when this rental completes.</p>
|
|
<p><a href="${frontendUrl}/earnings" style="color: #155724; text-decoration: underline;">View your earnings dashboard →</a></p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Format delivery method for display
|
|
const deliveryMethodDisplay = rental.deliveryMethod === "delivery" ? "Delivery" : "Pickup";
|
|
|
|
const variables = {
|
|
ownerName: owner.firstName || "there",
|
|
itemName: rental.item?.name || "your item",
|
|
renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter",
|
|
startDate: rental.startDateTime
|
|
? new Date(rental.startDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified",
|
|
endDate: rental.endDateTime
|
|
? new Date(rental.endDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified",
|
|
deliveryMethod: deliveryMethodDisplay,
|
|
paymentMessage: paymentMessage,
|
|
earningsSection: earningsSection,
|
|
stripeSection: stripeSection,
|
|
rentalDetailsUrl: `${frontendUrl}/my-listings?rentalId=${rental.id}`,
|
|
};
|
|
|
|
const htmlContent = this.renderTemplate(
|
|
"rentalApprovalConfirmationToOwner",
|
|
variables
|
|
);
|
|
|
|
const subject = `Rental Approved - ${rental.item?.name || "Your Item"}`;
|
|
|
|
return await this.sendEmail(owner.email, subject, htmlContent);
|
|
}
|
|
|
|
async sendRentalCompletionEmails(rental) {
|
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
|
const results = {
|
|
renterEmailSent: false,
|
|
ownerEmailSent: false,
|
|
};
|
|
|
|
try {
|
|
// Fetch owner details with Stripe info
|
|
const owner = await User.findByPk(rental.ownerId, {
|
|
attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"],
|
|
});
|
|
|
|
// Fetch renter details
|
|
const renter = await User.findByPk(rental.renterId, {
|
|
attributes: ["email", "firstName", "lastName"],
|
|
});
|
|
|
|
if (!owner || !renter) {
|
|
console.error(
|
|
"Owner or renter not found for rental completion emails"
|
|
);
|
|
return { success: false, error: "User not found" };
|
|
}
|
|
|
|
// Format dates
|
|
const startDate = rental.startDateTime
|
|
? new Date(rental.startDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified";
|
|
const endDate = rental.endDateTime
|
|
? new Date(rental.endDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: "Not specified";
|
|
const returnedDate = rental.actualReturnDateTime
|
|
? new Date(rental.actualReturnDateTime).toLocaleString("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
})
|
|
: endDate;
|
|
|
|
// Check if renter has already submitted a review
|
|
const hasReviewed = !!rental.itemReviewSubmittedAt;
|
|
|
|
// Build review section for renter email
|
|
let reviewSection = "";
|
|
if (!hasReviewed) {
|
|
reviewSection = `
|
|
<h2>Share Your Experience</h2>
|
|
<div class="info-box">
|
|
<p><strong>Help the community by leaving a review!</strong></p>
|
|
<p>Your feedback helps other renters make informed decisions and supports quality listings on RentAll.</p>
|
|
<ul>
|
|
<li>How was the item's condition?</li>
|
|
<li>Was the owner responsive and helpful?</li>
|
|
<li>Would you rent this item again?</li>
|
|
</ul>
|
|
</div>
|
|
<p style="text-align: center;">
|
|
<a href="${frontendUrl}/my-rentals?rentalId=${rental.id}&action=review" class="button">Leave a Review</a>
|
|
</p>
|
|
`;
|
|
} else {
|
|
reviewSection = `
|
|
<div class="success-box">
|
|
<p><strong>✓ Thank You for Your Review!</strong></p>
|
|
<p>Your feedback has been submitted and helps strengthen the RentAll community.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Send email to renter
|
|
try {
|
|
const renterVariables = {
|
|
renterName: renter.firstName || "there",
|
|
itemName: rental.item?.name || "the item",
|
|
ownerName: owner.firstName || "the owner",
|
|
startDate: startDate,
|
|
endDate: endDate,
|
|
returnedDate: returnedDate,
|
|
reviewSection: reviewSection,
|
|
browseItemsUrl: `${frontendUrl}/`,
|
|
};
|
|
|
|
const renterHtmlContent = this.renderTemplate(
|
|
"rentalCompletionThankYouToRenter",
|
|
renterVariables
|
|
);
|
|
|
|
const renterResult = await this.sendEmail(
|
|
renter.email,
|
|
`Thank You for Returning "${rental.item?.name || "Item"}" On Time!`,
|
|
renterHtmlContent
|
|
);
|
|
|
|
if (renterResult.success) {
|
|
console.log(
|
|
`Rental completion thank you email sent to renter: ${renter.email}`
|
|
);
|
|
results.renterEmailSent = true;
|
|
} else {
|
|
console.error(
|
|
`Failed to send rental completion email to renter (${renter.email}):`,
|
|
renterResult.error
|
|
);
|
|
}
|
|
} catch (emailError) {
|
|
console.error(
|
|
`Failed to send rental completion email to renter (${renter.email}):`,
|
|
emailError.message
|
|
);
|
|
}
|
|
|
|
// Prepare owner email
|
|
const hasStripeAccount = !!owner.stripeConnectedAccountId;
|
|
const totalAmount = parseFloat(rental.totalAmount) || 0;
|
|
const payoutAmount = parseFloat(rental.payoutAmount) || 0;
|
|
const platformFee = parseFloat(rental.platformFee) || 0;
|
|
const isPaidRental = totalAmount > 0;
|
|
|
|
// Build earnings section for owner (only for paid rentals)
|
|
let earningsSection = "";
|
|
if (isPaidRental) {
|
|
earningsSection = `
|
|
<h2>Your Earnings</h2>
|
|
<table class="info-table">
|
|
<tr>
|
|
<th>Total Rental Amount</th>
|
|
<td>\$${totalAmount.toFixed(2)}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Platform Fee (20%)</th>
|
|
<td>-\$${platformFee.toFixed(2)}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Your Payout</th>
|
|
<td class="highlight">\$${payoutAmount.toFixed(2)}</td>
|
|
</tr>
|
|
</table>
|
|
<p style="font-size: 14px; color: #6c757d;">
|
|
Your earnings will be automatically transferred to your account when the rental period ends and any dispute windows close.
|
|
</p>
|
|
`;
|
|
}
|
|
|
|
// Build Stripe section for owner
|
|
let stripeSection = "";
|
|
if (!hasStripeAccount && isPaidRental) {
|
|
// Show Stripe setup reminder for paid rentals
|
|
stripeSection = `
|
|
<div class="warning-box">
|
|
<p><strong>⚠️ Action Required: Set Up Your Earnings Account</strong></p>
|
|
<p>To receive your payout of <strong>\$${payoutAmount.toFixed(2)}</strong>, you need to set up your earnings account.</p>
|
|
</div>
|
|
<h2>Set Up Earnings to Get Paid</h2>
|
|
<div class="info-box">
|
|
<p><strong>Why set up now?</strong></p>
|
|
<ul>
|
|
<li><strong>Automatic payouts</strong> when the rental period ends</li>
|
|
<li><strong>Secure transfers</strong> directly to your bank account</li>
|
|
<li><strong>Track all earnings</strong> in one dashboard</li>
|
|
<li><strong>Fast deposits</strong> (typically 2-3 business days)</li>
|
|
</ul>
|
|
<p>Setup only takes about 5 minutes and you only need to do it once.</p>
|
|
</div>
|
|
<p style="text-align: center;">
|
|
<a href="${frontendUrl}/earnings" class="button">Set Up Earnings Account Now</a>
|
|
</p>
|
|
<p style="text-align: center; font-size: 14px; color: #856404;">
|
|
<strong>Important:</strong> Without earnings setup, you won't receive payouts automatically.
|
|
</p>
|
|
`;
|
|
} else if (hasStripeAccount && isPaidRental) {
|
|
stripeSection = `
|
|
<div class="success-box">
|
|
<p><strong>✓ Earnings Account Active</strong></p>
|
|
<p>Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when the rental period ends.</p>
|
|
<p><a href="${frontendUrl}/earnings" style="color: #155724; text-decoration: underline;">View your earnings dashboard →</a></p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Send email to owner
|
|
try {
|
|
const ownerVariables = {
|
|
ownerName: owner.firstName || "there",
|
|
itemName: rental.item?.name || "your item",
|
|
renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter",
|
|
startDate: startDate,
|
|
endDate: endDate,
|
|
returnedDate: returnedDate,
|
|
earningsSection: earningsSection,
|
|
stripeSection: stripeSection,
|
|
myListingsUrl: `${frontendUrl}/my-listings`,
|
|
};
|
|
|
|
const ownerHtmlContent = this.renderTemplate(
|
|
"rentalCompletionCongratsToOwner",
|
|
ownerVariables
|
|
);
|
|
|
|
const ownerResult = await this.sendEmail(
|
|
owner.email,
|
|
`Rental Complete - ${rental.item?.name || "Your Item"}`,
|
|
ownerHtmlContent
|
|
);
|
|
|
|
if (ownerResult.success) {
|
|
console.log(
|
|
`Rental completion congratulations email sent to owner: ${owner.email}`
|
|
);
|
|
results.ownerEmailSent = true;
|
|
} else {
|
|
console.error(
|
|
`Failed to send rental completion email to owner (${owner.email}):`,
|
|
ownerResult.error
|
|
);
|
|
}
|
|
} catch (emailError) {
|
|
console.error(
|
|
`Failed to send rental completion email to owner (${owner.email}):`,
|
|
emailError.message
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error sending rental completion emails:", error);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
async sendFeedbackConfirmation(user, feedback) {
|
|
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 = this.renderTemplate(
|
|
"feedbackConfirmationToUser",
|
|
variables
|
|
);
|
|
|
|
return await this.sendEmail(
|
|
user.email,
|
|
"Thank You for Your Feedback - RentAll",
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
async sendFeedbackNotificationToAdmin(user, feedback) {
|
|
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 = this.renderTemplate(
|
|
"feedbackNotificationToAdmin",
|
|
variables
|
|
);
|
|
|
|
return await this.sendEmail(
|
|
adminEmail,
|
|
`New Feedback from ${user.firstName} ${user.lastName}`,
|
|
htmlContent
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = new EmailService();
|