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", ]; 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(/]*>[\s\S]*?<\/style>/gi, "") .replace(/]*>[\s\S]*?<\/script>/gi, "") // Convert common HTML elements to text equivalents .replace(//gi, "\n") .replace(/<\/p>/gi, "\n\n") .replace(/<\/div>/gi, "\n") .replace(/<\/li>/gi, "\n") .replace(/<\/h[1-6]>/gi, "\n\n") .replace(/
  • /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 = ` {{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 RentAll!

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

    Hi {{recipientName}},

    Verify Your Email Address

    Thank you for registering with RentAll! 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 RentAll 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 RentAll 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}}

    Renter Notes: {{rentalNotes}}

    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}}

    Platform Fee (20%): -\${{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 RentAll 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 RentAll. 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 RentAll 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

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

    {{title}}

    {{message}}

    ` ) ); } 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 = `

    Payment Receipt

    💳

    Payment Successful

    Your payment has been processed. This email serves as your receipt.

    Amount Charged $${totalAmount.toFixed(2)}
    Payment Method ${paymentMethodDisplay}
    Transaction ID ${rental.stripePaymentIntentId || "N/A"}
    Transaction Date ${chargedAtFormatted}

    Note: Keep this email for your records. You can use the transaction ID above if you need to contact support about this payment.

    `; } else if (totalAmount === 0) { // Free rental message paymentSection = `

    No Payment Required: This is a free rental.

    `; } } 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 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 ? `

    Message from the owner:

    ${declineReason}

    ` : ""; 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 ${itemName}. We apologize for any inconvenience this may cause.`; // Only show refund info if rental had a cost if (rental.totalAmount > 0) { additionalInfo = `

    Full Refund Processed

    You will receive a full refund of $${refundInfo.amount.toFixed(2)}. The refund will appear in your account within 5-10 business days.

    Browse Other Items
    `; } else { additionalInfo = `

    This rental has been cancelled by the owner. We apologize for any inconvenience.

    Browse Other Items
    `; } } 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 ${itemName}.`; additionalInfo = `

    Your item is now available

    Your item is now available for other renters to book for these dates.

    `; } // 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 = `

    Refund Information

    $${refundInfo.amount.toFixed(2)}

    Refund Amount: $${refundInfo.amount.toFixed(2)} (${refundPercentage}% of total)

    Reason: ${refundInfo.reason}

    Processing Time: Refunds typically appear within 5-10 business days.

    `; } else { refundSection = `

    Refund Information

    No Refund Available

    ${refundInfo.reason}

    `; } } // 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 = `

    Your Earnings

    Total Rental Amount \$${totalAmount.toFixed(2)}
    Platform Fee (20%) -\$${platformFee.toFixed(2)}
    Your Payout \$${payoutAmount.toFixed(2)}
    `; } // Build conditional Stripe section based on Stripe status let stripeSection = ""; if (!hasStripeAccount && isPaidRental) { // Only show Stripe setup reminder for paid rentals stripeSection = `

    ⚠️ Action Required: Set Up Your Earnings Account

    To receive your payout of \$${payoutAmount.toFixed(2)} when this rental completes, you need to set up your earnings account.

    Set Up Earnings to Get Paid

    Why set up now?

    • Automatic payouts when rentals complete
    • Secure transfers directly to your bank account
    • Track all earnings in one dashboard
    • Fast deposits (typically 2-3 business days)

    Setup only takes about 5 minutes and you only need to do it once.

    Set Up Earnings Account Now

    Important: Without earnings setup, you won't receive payouts automatically when rentals complete.

    `; } else if (hasStripeAccount && isPaidRental) { stripeSection = `

    ✓ Earnings Account Active

    Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when this rental completes.

    View your earnings dashboard →

    `; } // 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 = `

    Share Your Experience

    Help the community by leaving a review!

    Your feedback helps other renters make informed decisions and supports quality listings on RentAll.

    • How was the item's condition?
    • Was the owner responsive and helpful?
    • Would you rent this item again?

    Leave a Review

    `; } else { reviewSection = `

    ✓ Thank You for Your Review!

    Your feedback has been submitted and helps strengthen the RentAll community.

    `; } // 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 = `

    Your Earnings

    Total Rental Amount \$${totalAmount.toFixed(2)}
    Platform Fee (20%) -\$${platformFee.toFixed(2)}
    Your Payout \$${payoutAmount.toFixed(2)}

    Your earnings will be automatically transferred to your account when the rental period ends and any dispute windows close.

    `; } // Build Stripe section for owner let stripeSection = ""; if (!hasStripeAccount && isPaidRental) { // Show Stripe setup reminder for paid rentals stripeSection = `

    ⚠️ Action Required: Set Up Your Earnings Account

    To receive your payout of \$${payoutAmount.toFixed(2)}, you need to set up your earnings account.

    Set Up Earnings to Get Paid

    Why set up now?

    • Automatic payouts when the rental period ends
    • Secure transfers directly to your bank account
    • Track all earnings in one dashboard
    • Fast deposits (typically 2-3 business days)

    Setup only takes about 5 minutes and you only need to do it once.

    Set Up Earnings Account Now

    Important: Without earnings setup, you won't receive payouts automatically.

    `; } else if (hasStripeAccount && isPaidRental) { stripeSection = `

    ✓ Earnings Account Active

    Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when the rental period ends.

    View your earnings dashboard →

    `; } // 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; } } module.exports = new EmailService();