integrated email is forum posts/comments

This commit is contained in:
jackiettran
2025-11-14 15:51:25 -05:00
parent 105f257c5f
commit 629f0055a1
6 changed files with 1647 additions and 61 deletions

View File

@@ -55,6 +55,10 @@ class EmailService {
"feedbackConfirmationToUser.html",
"feedbackNotificationToAdmin.html",
"newMessageToUser.html",
"forumCommentToPostAuthor.html",
"forumReplyToCommentAuthor.html",
"forumAnswerAcceptedToCommentAuthor.html",
"forumThreadActivityToParticipant.html",
];
for (const templateFile of templateFiles) {
@@ -65,14 +69,23 @@ class EmailService {
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.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`);
console.log(
`Loaded ${this.templates.size} of ${templateFiles.length} email templates`
);
} catch (error) {
console.warn("Templates directory not found, using fallback templates");
console.error("Failed to load email templates:", error);
console.error("Templates directory:", templatesDir);
console.error("Error stack:", error.stack);
}
}
@@ -178,19 +191,39 @@ class EmailService {
}
}
renderTemplate(templateName, variables = {}) {
async renderTemplate(templateName, variables = {}) {
// Ensure service is initialized before rendering
if (!this.initialized) {
console.log(`Email service 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;
Object.keys(variables).forEach((key) => {
const regex = new RegExp(`{{${key}}}`, "g");
rendered = rendered.replace(regex, variables[key] || "");
});
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;
}
@@ -504,7 +537,7 @@ class EmailService {
: "Not specified",
};
const htmlContent = this.renderTemplate(
const htmlContent = await this.renderTemplate(
"conditionCheckReminderToUser",
variables
);
@@ -543,25 +576,26 @@ class EmailService {
let paymentSection = "";
if (isRenter) {
const totalAmount = parseFloat(rental.totalAmount) || 0;
const isPaidRental = totalAmount > 0 && rental.paymentStatus === 'paid';
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);
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"
timeStyle: "short",
})
: new Date().toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short"
timeStyle: "short",
});
// Build payment receipt section HTML
@@ -583,7 +617,9 @@ class EmailService {
</tr>
<tr>
<th>Transaction ID</th>
<td style="font-family: monospace; font-size: 12px;">${rental.stripePaymentIntentId || "N/A"}</td>
<td style="font-family: monospace; font-size: 12px;">${
rental.stripePaymentIntentId || "N/A"
}</td>
</tr>
<tr>
<th>Transaction Date</th>
@@ -606,7 +642,10 @@ class EmailService {
variables.paymentSection = paymentSection;
const htmlContent = this.renderTemplate("rentalConfirmationToUser", variables);
const htmlContent = await this.renderTemplate(
"rentalConfirmationToUser",
variables
);
// Use clear, transactional subject line with item name
const subject = `Rental Confirmation - ${itemName}`;
@@ -623,7 +662,10 @@ class EmailService {
verificationUrl: verificationUrl,
};
const htmlContent = this.renderTemplate("emailVerificationToUser", variables);
const htmlContent = await this.renderTemplate(
"emailVerificationToUser",
variables
);
return await this.sendEmail(
user.email,
@@ -633,11 +675,6 @@ class EmailService {
}
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 = {
@@ -648,7 +685,10 @@ class EmailService {
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);
const htmlContent = await this.renderTemplate(
"alphaInvitationToUser",
variables
);
return await this.sendEmail(
email,
@@ -666,7 +706,10 @@ class EmailService {
resetUrl: resetUrl,
};
const htmlContent = this.renderTemplate("passwordResetToUser", variables);
const htmlContent = await this.renderTemplate(
"passwordResetToUser",
variables
);
return await this.sendEmail(
user.email,
@@ -687,7 +730,10 @@ class EmailService {
timestamp: timestamp,
};
const htmlContent = this.renderTemplate("passwordChangedToUser", variables);
const htmlContent = await this.renderTemplate(
"passwordChangedToUser",
variables
);
return await this.sendEmail(
user.email,
@@ -744,7 +790,10 @@ class EmailService {
approveUrl: approveUrl,
};
const htmlContent = this.renderTemplate("rentalRequestToOwner", variables);
const htmlContent = await this.renderTemplate(
"rentalRequestToOwner",
variables
);
return await this.sendEmail(
owner.email,
@@ -797,7 +846,7 @@ class EmailService {
viewRentalsUrl: viewRentalsUrl,
};
const htmlContent = this.renderTemplate(
const htmlContent = await this.renderTemplate(
"rentalRequestConfirmationToRenter",
variables
);
@@ -819,9 +868,7 @@ class EmailService {
});
if (!renter) {
console.error(
"Renter not found for rental decline notification"
);
console.error("Renter not found for rental decline notification");
return { success: false, error: "Renter not found" };
}
@@ -867,7 +914,7 @@ class EmailService {
totalAmount: totalAmount.toFixed(2),
};
const htmlContent = this.renderTemplate(
const htmlContent = await this.renderTemplate(
"rentalDeclinedToRenter",
variables
);
@@ -920,11 +967,16 @@ class EmailService {
earningsDashboardUrl: earningsDashboardUrl,
};
const htmlContent = this.renderTemplate("payoutReceivedToOwner", variables);
const htmlContent = await this.renderTemplate(
"payoutReceivedToOwner",
variables
);
return await this.sendEmail(
owner.email,
`Earnings Received - $${payoutAmount.toFixed(2)} for ${rental.item?.name || "Your Item"}`,
`Earnings Received - $${payoutAmount.toFixed(2)} for ${
rental.item?.name || "Your Item"
}`,
htmlContent
);
}
@@ -994,7 +1046,9 @@ class EmailService {
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>
<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>
@@ -1035,7 +1089,9 @@ class EmailService {
<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>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>
@@ -1063,7 +1119,7 @@ class EmailService {
refundSection: refundSection,
};
const confirmationHtml = this.renderTemplate(
const confirmationHtml = await this.renderTemplate(
"rentalCancellationConfirmationToUser",
confirmationVariables
);
@@ -1099,7 +1155,7 @@ class EmailService {
additionalInfo: additionalInfo,
};
const notificationHtml = this.renderTemplate(
const notificationHtml = await this.renderTemplate(
"rentalCancellationNotificationToUser",
notificationVariables
);
@@ -1112,7 +1168,9 @@ class EmailService {
if (notificationResult.success) {
console.log(
`Cancellation notification email sent to ${cancelledBy === "owner" ? "renter" : "owner"}: ${notificationRecipient}`
`Cancellation notification email sent to ${
cancelledBy === "owner" ? "renter" : "owner"
}: ${notificationRecipient}`
);
results.notificationEmailSent = true;
}
@@ -1130,7 +1188,7 @@ class EmailService {
}
async sendTemplateEmail(toEmail, subject, templateName, variables = {}) {
const htmlContent = this.renderTemplate(templateName, variables);
const htmlContent = await this.renderTemplate(templateName, variables);
return await this.sendEmail(toEmail, subject, htmlContent);
}
@@ -1413,7 +1471,7 @@ class EmailService {
viewItemUrl: `${frontendUrl}/items/${item.id}`,
};
const htmlContent = this.renderTemplate(
const htmlContent = await this.renderTemplate(
"firstListingCelebrationToOwner",
variables
);
@@ -1428,7 +1486,12 @@ class EmailService {
// Fetch owner details
const owner = await User.findByPk(rental.ownerId, {
attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"],
attributes: [
"email",
"firstName",
"lastName",
"stripeConnectedAccountId",
],
});
// Fetch renter details
@@ -1487,7 +1550,9 @@ class EmailService {
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>
<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">
@@ -1511,19 +1576,23 @@ class EmailService {
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>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 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",
renterName:
`${renter.firstName} ${renter.lastName}`.trim() || "The renter",
startDate: rental.startDateTime
? new Date(rental.startDateTime).toLocaleString("en-US", {
dateStyle: "medium",
@@ -1543,7 +1612,7 @@ class EmailService {
rentalDetailsUrl: `${frontendUrl}/owning?rentalId=${rental.id}`,
};
const htmlContent = this.renderTemplate(
const htmlContent = await this.renderTemplate(
"rentalApprovalConfirmationToOwner",
variables
);
@@ -1563,7 +1632,12 @@ class EmailService {
try {
// Fetch owner details with Stripe info
const owner = await User.findByPk(rental.ownerId, {
attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"],
attributes: [
"email",
"firstName",
"lastName",
"stripeConnectedAccountId",
],
});
// Fetch renter details
@@ -1572,9 +1646,7 @@ class EmailService {
});
if (!owner || !renter) {
console.error(
"Owner or renter not found for rental completion emails"
);
console.error("Owner or renter not found for rental completion emails");
return { success: false, error: "User not found" };
}
@@ -1641,7 +1713,7 @@ class EmailService {
browseItemsUrl: `${frontendUrl}/`,
};
const renterHtmlContent = this.renderTemplate(
const renterHtmlContent = await this.renderTemplate(
"rentalCompletionThankYouToRenter",
renterVariables
);
@@ -1709,7 +1781,9 @@ class EmailService {
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>
<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">
@@ -1733,7 +1807,9 @@ class EmailService {
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>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>
`;
@@ -1744,7 +1820,8 @@ class EmailService {
const ownerVariables = {
ownerName: owner.firstName || "there",
itemName: rental.item?.name || "your item",
renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter",
renterName:
`${renter.firstName} ${renter.lastName}`.trim() || "The renter",
startDate: startDate,
endDate: endDate,
returnedDate: returnedDate,
@@ -1753,7 +1830,7 @@ class EmailService {
owningUrl: `${frontendUrl}/owning`,
};
const ownerHtmlContent = this.renderTemplate(
const ownerHtmlContent = await this.renderTemplate(
"rentalCompletionCongratsToOwner",
ownerVariables
);
@@ -1802,7 +1879,7 @@ class EmailService {
year: new Date().getFullYear(),
};
const htmlContent = this.renderTemplate(
const htmlContent = await this.renderTemplate(
"feedbackConfirmationToUser",
variables
);
@@ -1815,7 +1892,8 @@ class EmailService {
}
async sendFeedbackNotificationToAdmin(user, feedback) {
const adminEmail = process.env.FEEDBACK_EMAIL || process.env.CUSTOMER_SUPPORT_EMAIL;
const adminEmail =
process.env.FEEDBACK_EMAIL || process.env.CUSTOMER_SUPPORT_EMAIL;
if (!adminEmail) {
console.warn("No admin email configured for feedback notifications");
@@ -1839,7 +1917,7 @@ class EmailService {
year: new Date().getFullYear(),
};
const htmlContent = this.renderTemplate(
const htmlContent = await this.renderTemplate(
"feedbackNotificationToAdmin",
variables
);
@@ -1870,7 +1948,10 @@ class EmailService {
timestamp: timestamp,
};
const htmlContent = this.renderTemplate("newMessageToUser", variables);
const htmlContent = await this.renderTemplate(
"newMessageToUser",
variables
);
const subject = `New message from ${sender.firstName} ${sender.lastName}`;
@@ -1888,6 +1969,207 @@ class EmailService {
return { success: false, error: error.message };
}
}
async sendForumCommentNotification(postAuthor, commenter, post, comment) {
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.renderTemplate(
"forumCommentToPostAuthor",
variables
);
const subject = `${commenter.firstName} ${commenter.lastName} commented on your post`;
const result = await this.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 };
}
}
async sendForumReplyNotification(
commentAuthor,
replier,
post,
reply,
parentComment
) {
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.renderTemplate(
"forumReplyToCommentAuthor",
variables
);
const subject = `${replier.firstName} ${replier.lastName} replied to your comment`;
const result = await this.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 };
}
}
async sendForumAnswerAcceptedNotification(
commentAuthor,
postAuthor,
post,
comment
) {
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.renderTemplate(
"forumAnswerAcceptedToCommentAuthor",
variables
);
const subject = `Your comment was marked as the accepted answer!`;
const result = await this.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 };
}
}
async sendForumThreadActivityNotification(
participant,
commenter,
post,
comment
) {
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.renderTemplate(
"forumThreadActivityToParticipant",
variables
);
const subject = `New activity on a post you're following`;
const result = await this.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 = new EmailService();