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", "emailVerification.html", "passwordReset.html", "passwordChanged.html", "lateReturnCS.html", "damageReportCS.html", "lostItemCS.html", "rentalRequest.html", "rentalRequestConfirmation.html", "rentalCancellationConfirmation.html", "rentalCancellationNotification.html", "rentalDeclined.html", "payoutReceived.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 = { conditionCheckReminder: baseTemplate.replace( "{{content}}", `

    {{title}}

    {{message}}

    Rental Item: {{itemName}}

    Deadline: {{deadline}}

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

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

    Hi {{recipientName}},

    {{title}}

    {{message}}

    Item: {{itemName}}

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

    Thank you for using RentAll!

    ` ), emailVerification: 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.

    ` ), passwordReset: 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.

    ` ), passwordChanged: 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.

    ` ), rentalRequest: 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.

    ` ), rentalRequestConfirmation: 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

    ` ), rentalCancellationConfirmation: 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}} ` ), rentalCancellationNotification: 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.

    ` ), payoutReceived: 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!

    ` ), rentalDeclined: 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.

    ` ), }; 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( "conditionCheckReminder", variables ); return await this.sendEmail( userEmail, `RentAll: ${notification.title}`, htmlContent ); } async sendRentalConfirmation( userEmail, notification, rental, recipientName = null ) { 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", }; const htmlContent = this.renderTemplate("rentalConfirmation", 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("emailVerification", 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("passwordReset", 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("passwordChanged", 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("rentalRequest", 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( "rentalRequestConfirmation", 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( "rentalDeclined", 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("payoutReceived", 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( "rentalCancellationConfirmation", 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( "rentalCancellationNotification", 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", "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 (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", "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) { 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 ); 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 ); 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; } } module.exports = new EmailService();