498 lines
15 KiB
JavaScript
498 lines
15 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 = [
|
|
"conditionCheckReminder.html",
|
|
"rentalConfirmation.html",
|
|
"lateReturnCS.html",
|
|
"damageReportCS.html",
|
|
"lostItemCS.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);
|
|
} catch (error) {
|
|
console.warn(`Template ${templateFile} not found, will use fallback`);
|
|
}
|
|
}
|
|
|
|
console.log(`Loaded ${this.templates.size} email templates`);
|
|
} catch (error) {
|
|
console.warn("Templates directory not found, using fallback templates");
|
|
}
|
|
}
|
|
|
|
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" };
|
|
}
|
|
|
|
const params = {
|
|
Source: process.env.SES_FROM_EMAIL,
|
|
Destination: {
|
|
ToAddresses: Array.isArray(to) ? to : [to],
|
|
},
|
|
Message: {
|
|
Subject: {
|
|
Data: subject,
|
|
Charset: "UTF-8",
|
|
},
|
|
Body: {
|
|
Html: {
|
|
Data: htmlContent,
|
|
Charset: "UTF-8",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
if (textContent) {
|
|
params.Message.Body.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 = {
|
|
conditionCheckReminder: 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>
|
|
`
|
|
),
|
|
|
|
rentalConfirmation: baseTemplate.replace(
|
|
"{{content}}",
|
|
`
|
|
<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>
|
|
`
|
|
),
|
|
|
|
damageClaimNotification: baseTemplate.replace(
|
|
"{{content}}",
|
|
`
|
|
<h2>{{title}}</h2>
|
|
<p>{{message}}</p>
|
|
<p><strong>Item:</strong> {{itemName}}</p>
|
|
<p><strong>Claim Amount:</strong> ${{ claimAmount }}</p>
|
|
<p><strong>Description:</strong> {{description}}</p>
|
|
<p>Please review this claim and respond accordingly through your account.</p>
|
|
`
|
|
),
|
|
|
|
returnIssueNotification: baseTemplate.replace(
|
|
"{{content}}",
|
|
`
|
|
<h2>{{title}}</h2>
|
|
<p>{{message}}</p>
|
|
<p><strong>Item:</strong> {{itemName}}</p>
|
|
<p><strong>Return Status:</strong> {{returnStatus}}</p>
|
|
<p>Please check your account for more details and take appropriate action.</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(
|
|
"conditionCheckReminder",
|
|
variables
|
|
);
|
|
|
|
return await this.sendEmail(
|
|
userEmail,
|
|
`RentAll: ${notification.title}`,
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
async sendRentalConfirmation(userEmail, notification, rental) {
|
|
const variables = {
|
|
title: notification.title,
|
|
message: notification.message,
|
|
itemName: rental?.item?.name || "Unknown Item",
|
|
startDate: rental?.startDateTime
|
|
? new Date(rental.startDateTime).toLocaleDateString()
|
|
: "Not specified",
|
|
endDate: rental?.endDateTime
|
|
? new Date(rental.endDateTime).toLocaleDateString()
|
|
: "Not specified",
|
|
};
|
|
|
|
const htmlContent = this.renderTemplate("rentalConfirmation", variables);
|
|
|
|
return await this.sendEmail(
|
|
userEmail,
|
|
`RentAll: ${notification.title}`,
|
|
htmlContent
|
|
);
|
|
}
|
|
|
|
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",
|
|
"lateReturnCS",
|
|
{
|
|
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
|
|
const damageFee = damageAssessment.feeCalculation.amount;
|
|
const lateFee = 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",
|
|
"damageReportCS",
|
|
{
|
|
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",
|
|
"lostItemCS",
|
|
{
|
|
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) {
|
|
try {
|
|
// Get owner and renter emails
|
|
const owner = await User.findByPk(rental.ownerId, {
|
|
attributes: ["email"],
|
|
});
|
|
const renter = await User.findByPk(rental.renterId, {
|
|
attributes: ["email"],
|
|
});
|
|
|
|
// 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
|
|
if (owner?.email) {
|
|
await this.sendRentalConfirmation(
|
|
owner.email,
|
|
ownerNotification,
|
|
rental
|
|
);
|
|
console.log(`Rental confirmation email sent to owner: ${owner.email}`);
|
|
}
|
|
|
|
// Send email to renter
|
|
if (renter?.email) {
|
|
await this.sendRentalConfirmation(
|
|
renter.email,
|
|
renterNotification,
|
|
rental
|
|
);
|
|
console.log(
|
|
`Rental confirmation email sent to renter: ${renter.email}`
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error sending rental confirmation emails:", error);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new EmailService();
|