const fs = require("fs").promises; const path = require("path"); const logger = require("../../../utils/logger"); /** * TemplateManager handles loading, caching, and rendering email templates * This class is responsible for: * - Loading HTML email templates from disk * - Caching templates in memory for performance * - Rendering templates with variable substitution * - Providing fallback templates when files can't be loaded */ class TemplateManager { constructor() { // Singleton pattern - return existing instance if already created if (TemplateManager.instance) { return TemplateManager.instance; } this.templates = new Map(); this.initialized = false; this.initializationPromise = null; TemplateManager.instance = this; } /** * Initialize the template manager by loading all email templates * @returns {Promise} */ async initialize() { // If already initialized, return immediately if (this.initialized) return; // If initialization is in progress, wait for it if (this.initializationPromise) { return this.initializationPromise; } // Start initialization and store the promise this.initializationPromise = (async () => { await this.loadEmailTemplates(); this.initialized = true; logger.info("Email Template Manager initialized successfully"); })(); return this.initializationPromise; } /** * Load all email templates from disk into memory * @returns {Promise} */ async loadEmailTemplates() { const templatesDir = path.join( __dirname, "..", "..", "..", "templates", "emails" ); // Critical templates that must load for the app to function const criticalTemplates = [ "emailVerificationToUser.html", "passwordResetToUser.html", "passwordChangedToUser.html", "personalInfoChangedToUser.html", ]; try { const templateFiles = [ "conditionCheckReminderToUser.html", "rentalConfirmationToUser.html", "emailVerificationToUser.html", "passwordResetToUser.html", "passwordChangedToUser.html", "personalInfoChangedToUser.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", "itemDeletionToOwner.html", "alphaInvitationToUser.html", "feedbackConfirmationToUser.html", "feedbackNotificationToAdmin.html", "newMessageToUser.html", "forumCommentToPostAuthor.html", "forumReplyToCommentAuthor.html", "forumAnswerAcceptedToCommentAuthor.html", "forumThreadActivityToParticipant.html", "forumPostClosed.html", "forumItemRequestNotification.html", "forumPostDeletionToAuthor.html", "forumCommentDeletionToAuthor.html", "paymentDeclinedToRenter.html", "paymentMethodUpdatedToOwner.html", "payoutFailedToOwner.html", "disputeAlertToAdmin.html", "disputeLostAlertToAdmin.html", "userBannedNotification.html", ]; const failedTemplates = []; 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); logger.debug("Loaded template", { templateName }); } catch (error) { logger.error("Failed to load template", { templateFile, templatePath: path.join(templatesDir, templateFile), error, }); failedTemplates.push(templateFile); } } logger.info("Loaded email templates", { loaded: this.templates.size, total: templateFiles.length, }); // Check if critical templates are missing const missingCriticalTemplates = criticalTemplates.filter( (template) => !this.templates.has(path.basename(template, ".html")) ); if (missingCriticalTemplates.length > 0) { const error = new Error( `Critical email templates failed to load: ${missingCriticalTemplates.join( ", " )}` ); error.missingTemplates = missingCriticalTemplates; throw error; } // Warn if non-critical templates failed if (failedTemplates.length > 0) { logger.warn("Non-critical templates failed to load, using fallback versions", { failedTemplates, }); } } catch (error) { logger.error("Failed to load email templates", { templatesDir, error, }); throw error; // Re-throw to fail server startup } } /** * Render a template with the provided variables * @param {string} templateName - Name of the template to render * @param {Object} variables - Variables to substitute in the template * @returns {Promise} Rendered HTML */ async renderTemplate(templateName, variables = {}) { // Ensure service is initialized before rendering if (!this.initialized) { logger.debug("Template manager not initialized yet, initializing now..."); await this.initialize(); } let template = this.templates.get(templateName); if (!template) { logger.error("Template not found, using fallback", { templateName, availableTemplates: Array.from(this.templates.keys()), }); template = this.getFallbackTemplate(templateName); } else { logger.debug("Template found", { templateName }); } let rendered = template; try { Object.keys(variables).forEach((key) => { const regex = new RegExp(`{{${key}}}`, "g"); rendered = rendered.replace(regex, variables[key] || ""); }); } catch (error) { logger.error("Error rendering template", { templateName, variableKeys: Object.keys(variables), error, }); } return rendered; } /** * Get a fallback template when the HTML file is not available * @param {string} templateName - Name of the template * @returns {string} Fallback HTML template */ getFallbackTemplate(templateName) { const baseTemplate = ` {{title}}
{{content}}
`; const templates = { conditionCheckReminderToUser: baseTemplate.replace( "{{content}}", `

{{title}}

{{message}}

Rental Item: {{itemName}}

Deadline: {{deadline}}

Please complete this condition check as soon as possible to ensure proper documentation.

` ), rentalConfirmationToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

{{title}}

{{message}}

Item: {{itemName}}

Rental Period: {{startDate}} to {{endDate}}

Thank you for using Village Share!

` ), emailVerificationToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

Verify Your Email Address

Thank you for registering with Village Share! Please verify your email address by clicking the button below.

Verify Email Address

If the button doesn't work, copy and paste this link into your browser: {{verificationUrl}}

This link will expire in 24 hours.

` ), passwordResetToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

Reset Your Password

We received a request to reset the password for your Village Share account. Click the button below to choose a new password.

Reset Password

If the button doesn't work, copy and paste this link into your browser: {{resetUrl}}

This link will expire in 1 hour.

If you didn't request this, you can safely ignore this email.

` ), passwordChangedToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

Your Password Has Been Changed

This is a confirmation that the password for your Village Share account ({{email}}) has been successfully changed.

Changed on: {{timestamp}}

For your security, all existing sessions have been logged out.

Didn't change your password? If you did not make this change, please contact our support team immediately.

` ), rentalRequestToOwner: baseTemplate.replace( "{{content}}", `

Hi {{ownerName}},

New Rental Request for {{itemName}}

{{renterName}} would like to rent your item.

Rental Period: {{startDate}} to {{endDate}}

Total Amount: \${{totalAmount}}

Your Earnings: \${{payoutAmount}}

Delivery Method: {{deliveryMethod}}

Intended Use: {{intendedUse}}

Review & Respond

Please respond to this request within 24 hours.

` ), rentalRequestConfirmationToRenter: baseTemplate.replace( "{{content}}", `

Hi {{renterName}},

Your Rental Request Has Been Submitted!

Your request to rent {{itemName}} has been sent to the owner.

Item: {{itemName}}

Rental Period: {{startDate}} to {{endDate}}

Delivery Method: {{deliveryMethod}}

Total Amount: \${{totalAmount}}

{{paymentMessage}}

You'll receive an email notification once the owner responds to your request.

View My Rentals

` ), rentalCancellationConfirmationToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

Rental Cancelled Successfully

This confirms that your rental for {{itemName}} has been cancelled.

Item: {{itemName}}

Start Date: {{startDate}}

End Date: {{endDate}}

Cancelled On: {{cancelledAt}}

{{refundSection}} ` ), rentalCancellationNotificationToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

Rental Cancellation Notice

{{cancellationMessage}}

Item: {{itemName}}

Start Date: {{startDate}}

End Date: {{endDate}}

Cancelled On: {{cancelledAt}}

{{additionalInfo}}

If you have any questions or concerns, please reach out to our support team.

` ), payoutReceivedToOwner: baseTemplate.replace( "{{content}}", `

Hi {{ownerName}},

Earnings Received: \${{payoutAmount}}

Great news! Your earnings from the rental of {{itemName}} have been transferred to your account.

Rental Details

Item: {{itemName}}

Rental Period: {{startDate}} to {{endDate}}

Transfer ID: {{stripeTransferId}}

Earnings Breakdown

Rental Amount: \${{totalAmount}}

Community Upkeep Fee (10%): -\${{platformFee}}

Your Earnings: \${{payoutAmount}}

Funds are typically available in your bank account within 2-3 business days.

View Earnings Dashboard

Thank you for being a valued member of the Village Share community!

` ), rentalDeclinedToRenter: baseTemplate.replace( "{{content}}", `

Hi {{renterName}},

Rental Request Declined

Thank you for your interest in renting {{itemName}}. Unfortunately, the owner is unable to accept your rental request at this time.

Request Details

Item: {{itemName}}

Start Date: {{startDate}}

End Date: {{endDate}}

Delivery Method: {{deliveryMethod}}

{{ownerMessage}}

What happens next?

{{paymentMessage}}

We encourage you to explore other similar items available for rent on Village Share. There are many great options waiting for you!

Browse Available Items

If you have any questions or concerns, please don't hesitate to contact our support team.

` ), rentalApprovalConfirmationToOwner: baseTemplate.replace( "{{content}}", `

Hi {{ownerName}},

You've Approved the Rental Request!

You've successfully approved the rental request for {{itemName}}.

Rental Details

Item: {{itemName}}

Renter: {{renterName}}

Start Date: {{startDate}}

End Date: {{endDate}}

Your Earnings: \${{payoutAmount}}

{{stripeSection}}

What's Next?

  • Coordinate with the renter on pickup details
  • Take photos of the item's condition before handoff
  • Provide any care instructions or usage tips

View Rental Details

` ), rentalCompletionThankYouToRenter: baseTemplate.replace( "{{content}}", `

Hi {{renterName}},

Thank You for Returning On Time!

You've successfully returned {{itemName}} on time. On-time returns like yours help build trust in the Village Share community!

Rental Summary

Item: {{itemName}}

Rental Period: {{startDate}} to {{endDate}}

Returned On: {{returnedDate}}

{{reviewSection}}

Browse Available Items

` ), rentalCompletionCongratsToOwner: baseTemplate.replace( "{{content}}", `

Hi {{ownerName}},

Congratulations on Completing a Rental!

{{itemName}} has been successfully returned on time. Great job!

Rental Summary

Item: {{itemName}}

Renter: {{renterName}}

Rental Period: {{startDate}} to {{endDate}}

{{earningsSection}} {{stripeSection}}

View My Listings

` ), feedbackConfirmationToUser: baseTemplate.replace( "{{content}}", `

Hi {{userName}},

Thank You for Your Feedback!

We've received your feedback and our team will review it carefully.

{{feedbackText}}

Submitted: {{submittedAt}}

Your input helps us improve Village Share for everyone. We take all feedback seriously and use it to make the platform better.

If your feedback requires a response, our team will reach out to you directly.

` ), feedbackNotificationToAdmin: baseTemplate.replace( "{{content}}", `

New Feedback Received

From: {{userName}} ({{userEmail}})

User ID: {{userId}}

Submitted: {{submittedAt}}

Feedback Content

{{feedbackText}}

Technical Context

Feedback ID: {{feedbackId}}

Page URL: {{url}}

User Agent: {{userAgent}}

Please review this feedback and take appropriate action if needed.

` ), paymentDeclinedToRenter: baseTemplate.replace( "{{content}}", `

Hi {{renterFirstName}},

Payment Issue with Your Rental Request

The owner tried to approve your rental for {{itemName}}, but there was an issue processing your payment.

What Happened

{{declineReason}}

What You Can Do

Please update your payment method so the owner can complete the approval of your rental request.

Once you update your payment method, the owner will be notified and can try to approve your rental again.

` ), paymentMethodUpdatedToOwner: baseTemplate.replace( "{{content}}", `

Hi {{ownerFirstName}},

Payment Method Updated

The renter has updated their payment method for the rental of {{itemName}}.

Ready to Approve

You can now try approving the rental request again. The renter's new payment method will be charged when you approve.

Review & Approve Rental

` ), userBannedNotification: baseTemplate.replace( "{{content}}", `

Hi {{userName}},

Your Account Has Been Suspended

Your Village Share account has been suspended by our moderation team.

Reason for Suspension:

{{banReason}}

You have been logged out of all devices and cannot log in to your account.

If you believe this suspension was made in error, please contact {{supportEmail}}.

` ), }; return ( templates[templateName] || baseTemplate.replace( "{{content}}", `

{{title}}

{{message}}

` ) ); } } module.exports = TemplateManager;