Files
rentall-app/backend/services/email/domain/RentalFlowEmailService.js
2026-01-08 12:44:57 -05:00

1259 lines
47 KiB
JavaScript

const EmailClient = require("../core/EmailClient");
const TemplateManager = require("../core/TemplateManager");
const logger = require("../../../utils/logger");
/**
* RentalFlowEmailService handles rental lifecycle flow emails
* This service is responsible for:
* - Sending rental request notifications to owners
* - Sending rental request confirmations to renters
* - Sending rental approval confirmations to owners
* - Sending rental declined notifications to renters
* - Sending rental confirmation emails to renters and owners
* - Sending rental cancellation emails to both parties
* - Sending rental completion emails to both parties
* - Sending payout received notifications to owners
*/
class RentalFlowEmailService {
constructor() {
this.emailClient = new EmailClient();
this.templateManager = new TemplateManager();
this.initialized = false;
}
/**
* Initialize the rental flow email service
* @returns {Promise<void>}
*/
async initialize() {
if (this.initialized) return;
await Promise.all([
this.emailClient.initialize(),
this.templateManager.initialize(),
]);
this.initialized = true;
console.log("Rental Flow Email Service initialized successfully");
}
/**
* Send rental request email to owner
* @param {Object} owner - Owner user object
* @param {string} owner.email - Owner's email address
* @param {string} owner.firstName - Owner's first name
* @param {Object} renter - Renter user object
* @param {string} renter.firstName - Renter's first name
* @param {string} renter.lastName - Renter's last name
* @param {Object} rental - Rental object with all details
* @param {number} rental.id - Rental ID
* @param {Object} rental.item - Item object
* @param {string} rental.item.name - Item name
* @param {string} rental.startDateTime - Rental start date
* @param {string} rental.endDateTime - Rental end date
* @param {string} rental.totalAmount - Total rental amount
* @param {string} rental.payoutAmount - Owner's payout amount
* @param {string} rental.deliveryMethod - Delivery method
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
*/
async sendRentalRequestEmail(owner, renter, rental) {
if (!this.initialized) {
await this.initialize();
}
try {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const approveUrl = `${frontendUrl}/owning?rentalId=${rental.id}`;
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",
intendedUse: rental.intendedUse || "Not specified",
approveUrl: approveUrl,
};
const htmlContent = await this.templateManager.renderTemplate(
"rentalRequestToOwner",
variables
);
return await this.emailClient.sendEmail(
owner.email,
`Rental Request for ${rental.item?.name || "Your Item"}`,
htmlContent
);
} catch (error) {
console.error("Failed to send rental request email:", error);
return { success: false, error: error.message };
}
}
/**
* Send rental request confirmation email to renter
* @param {Object} renter - Renter user object
* @param {string} renter.email - Renter's email address
* @param {string} renter.firstName - Renter's first name
* @param {Object} rental - Rental object with all details
* @param {Object} rental.item - Item object
* @param {string} rental.item.name - Item name
* @param {string} rental.startDateTime - Rental start date
* @param {string} rental.endDateTime - Rental end date
* @param {string} rental.totalAmount - Total rental amount
* @param {string} rental.deliveryMethod - Delivery method
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
*/
async sendRentalRequestConfirmationEmail(renter, rental) {
if (!this.initialized) {
await this.initialize();
}
try {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const viewRentalsUrl = `${frontendUrl}/renting`;
// 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 = await this.templateManager.renderTemplate(
"rentalRequestConfirmationToRenter",
variables
);
return await this.emailClient.sendEmail(
renter.email,
`Rental Request Submitted - ${rental.item?.name || "Item"}`,
htmlContent
);
} catch (error) {
console.error("Failed to send rental request confirmation email:", error);
return { success: false, error: error.message };
}
}
/**
* Send rental approval confirmation email to owner
* @param {Object} owner - Owner user object
* @param {string} owner.email - Owner's email address
* @param {string} owner.firstName - Owner's first name
* @param {string} owner.stripeConnectedAccountId - Owner's Stripe account ID
* @param {Object} renter - Renter user object
* @param {string} renter.firstName - Renter's first name
* @param {string} renter.lastName - Renter's last name
* @param {Object} rental - Rental object with all details
* @param {number} rental.id - Rental ID
* @param {Object} rental.item - Item object
* @param {string} rental.item.name - Item name
* @param {string} rental.startDateTime - Rental start date
* @param {string} rental.endDateTime - Rental end date
* @param {string} rental.deliveryMethod - Delivery method
* @param {string} rental.totalAmount - Total rental amount
* @param {string} rental.payoutAmount - Owner's payout amount
* @param {string} rental.platformFee - Platform fee amount
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
*/
async sendRentalApprovalConfirmationEmail(owner, renter, rental) {
if (!this.initialized) {
await this.initialize();
}
try {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
// 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 = `
<h2>Your Earnings</h2>
<table class="info-table">
<tr>
<th>Total Rental Amount</th>
<td>$${totalAmount.toFixed(2)}</td>
</tr>
<tr>
<th>Community Upkeep Fee (10%)</th>
<td>-$${platformFee.toFixed(2)}</td>
</tr>
<tr>
<th>Your Payout</th>
<td class="highlight">$${payoutAmount.toFixed(2)}</td>
</tr>
</table>
`;
}
// Build conditional Stripe section based on Stripe status
let stripeSection = "";
if (!hasStripeAccount && isPaidRental) {
// Only show Stripe setup reminder for paid rentals
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>
</div>
<h2>Set Up Earnings to Get Paid</h2>
<div class="info-box">
<p><strong>Why set up now?</strong></p>
<ul>
<li><strong>Automatic payouts</strong> when rentals complete</li>
<li><strong>Secure transfers</strong> directly to your bank account</li>
<li><strong>Track all earnings</strong> in one dashboard</li>
<li><strong>Fast deposits</strong> (typically 2-7 business days)</li>
</ul>
<p>Setup only takes about 5 minutes and you only need to do it once.</p>
</div>
<p style="text-align: center;">
<a href="${frontendUrl}/earnings" class="button">Set Up Earnings Account Now</a>
</p>
<p style="text-align: center; font-size: 14px; color: #856404;">
<strong>Important:</strong> Without earnings setup, you won't receive payouts automatically when rentals complete.
</p>
`;
} else if (hasStripeAccount && isPaidRental) {
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><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 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}/owning?rentalId=${rental.id}`,
};
const htmlContent = await this.templateManager.renderTemplate(
"rentalApprovalConfirmationToOwner",
variables
);
const subject = `Rental Approved - ${rental.item?.name || "Your Item"}`;
return await this.emailClient.sendEmail(
owner.email,
subject,
htmlContent
);
} catch (error) {
console.error(
"Failed to send rental approval confirmation email:",
error
);
return { success: false, error: error.message };
}
}
/**
* Send rental declined email to renter
* @param {Object} renter - Renter user object
* @param {string} renter.email - Renter's email address
* @param {string} renter.firstName - Renter's first name
* @param {Object} rental - Rental object with all details
* @param {Object} rental.item - Item object
* @param {string} rental.item.name - Item name
* @param {string} rental.startDateTime - Rental start date
* @param {string} rental.endDateTime - Rental end date
* @param {string} rental.totalAmount - Total rental amount
* @param {string} rental.payoutAmount - Owner's payout amount
* @param {string} rental.deliveryMethod - Delivery method
* @param {string|null} declineReason - Reason for declining rental (optional)
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
*/
async sendRentalDeclinedEmail(renter, rental, declineReason) {
if (!this.initialized) {
await this.initialize();
}
try {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const browseItemsUrl = `${frontendUrl}/`;
// 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
? `
<div class="notice-box">
<p><strong>Message from the owner:</strong></p>
<p>${declineReason}</p>
</div>
`
: "";
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 = await this.templateManager.renderTemplate(
"rentalDeclinedToRenter",
variables
);
return await this.emailClient.sendEmail(
renter.email,
`Rental Request Declined - ${rental.item?.name || "Item"}`,
htmlContent
);
} catch (error) {
console.error("Failed to send rental declined email:", error);
return { success: false, error: error.message };
}
}
/**
* Send rental confirmation email with payment receipt (if applicable)
* @param {string} userEmail - User's email address
* @param {Object} notification - Notification object
* @param {string} notification.title - Notification title
* @param {string} notification.message - Notification message
* @param {Object} rental - Rental object with all rental details
* @param {Object} rental.item - Item object
* @param {string} rental.item.name - Item name
* @param {string} rental.startDateTime - Rental start date
* @param {string} rental.endDateTime - Rental end date
* @param {string} rental.totalAmount - Total rental amount
* @param {string} rental.paymentStatus - Payment status
* @param {string} rental.paymentMethodBrand - Payment method brand
* @param {string} rental.paymentMethodLast4 - Last 4 digits of payment method
* @param {string} rental.stripePaymentIntentId - Stripe payment intent ID
* @param {string} rental.chargedAt - Payment charge timestamp
* @param {string|null} recipientName - Recipient's name
* @param {boolean} isRenter - Whether recipient is the renter (to show payment receipt)
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
*/
async sendRentalConfirmation(
userEmail,
notification,
rental,
recipientName = null,
isRenter = false
) {
if (!this.initialized) {
await this.initialize();
}
try {
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 = `
<h2>Payment Receipt</h2>
<div class="success-box">
<div class="icon">💳</div>
<p><strong>Payment Successful</strong></p>
<p>Your payment has been processed. This email serves as your receipt.</p>
</div>
<table class="info-table">
<tr>
<th>Amount Charged</th>
<td><strong>$${totalAmount.toFixed(2)}</strong></td>
</tr>
<tr>
<th>Payment Method</th>
<td>${paymentMethodDisplay}</td>
</tr>
<tr>
<th>Transaction ID</th>
<td style="font-family: monospace; font-size: 12px;">${
rental.stripePaymentIntentId || "N/A"
}</td>
</tr>
<tr>
<th>Transaction Date</th>
<td>${chargedAtFormatted}</td>
</tr>
</table>
<p style="font-size: 13px; color: #6c757d; margin-top: 10px;">
<strong>Note:</strong> Keep this email for your records. You can use the transaction ID above if you need to contact support about this payment.
</p>
`;
} else if (totalAmount === 0) {
// Free rental message
paymentSection = `
<div class="success-box">
<p><strong>No Payment Required:</strong> This is a free rental.</p>
</div>
`;
}
}
variables.paymentSection = paymentSection;
const htmlContent = await this.templateManager.renderTemplate(
"rentalConfirmationToUser",
variables
);
// Use clear, transactional subject line with item name
const subject = `Rental Confirmation - ${itemName}`;
return await this.emailClient.sendEmail(userEmail, subject, htmlContent);
} catch (error) {
console.error("Failed to send rental confirmation:", error);
return { success: false, error: error.message };
}
}
/**
* Send rental confirmation emails to both owner and renter
* @param {Object} owner - Owner user object
* @param {string} owner.email - Owner's email address
* @param {string} owner.firstName - Owner's first name
* @param {Object} renter - Renter user object
* @param {string} renter.email - Renter's email address
* @param {string} renter.firstName - Renter's first name
* @param {Object} rental - Rental object with all details
* @param {number} rental.id - Rental ID
* @param {number} rental.ownerId - Owner's user ID
* @param {number} rental.renterId - Renter's user ID
* @param {Object} rental.item - Item object
* @param {string} rental.item.name - Item name
* @param {string} rental.startDateTime - Rental start date
* @returns {Promise<{ownerEmailSent: boolean, renterEmailSent: boolean}>}
*/
async sendRentalConfirmationEmails(owner, renter, rental) {
if (!this.initialized) {
await this.initialize();
}
const results = {
ownerEmailSent: false,
renterEmailSent: false,
};
try {
// 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;
}
/**
* Send rental cancellation emails to both parties
* @param {Object} owner - Owner user object
* @param {string} owner.email - Owner's email address
* @param {string} owner.firstName - Owner's first name
* @param {Object} renter - Renter user object
* @param {string} renter.email - Renter's email address
* @param {string} renter.firstName - Renter's first name
* @param {Object} rental - Rental object with all details
* @param {Object} rental.item - Item object
* @param {string} rental.item.name - Item name
* @param {string} rental.startDateTime - Rental start date
* @param {string} rental.endDateTime - Rental end date
* @param {string} rental.cancelledBy - Who cancelled ('owner' or 'renter')
* @param {string} rental.cancelledAt - Cancellation timestamp
* @param {string} rental.totalAmount - Total rental amount
* @param {Object} refundInfo - Refund information
* @param {number} refundInfo.amount - Refund amount
* @param {number} refundInfo.percentage - Refund percentage (0-1)
* @param {string} refundInfo.reason - Refund reason description
* @returns {Promise<{confirmationEmailSent: boolean, notificationEmailSent: boolean}>}
*/
async sendRentalCancellationEmails(owner, renter, rental, refundInfo) {
if (!this.initialized) {
await this.initialize();
}
const results = {
confirmationEmailSent: false,
notificationEmailSent: false,
};
try {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const browseUrl = `${frontendUrl}/`;
const cancelledBy = rental.cancelledBy;
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 <strong>${itemName}</strong>. We apologize for any inconvenience this may cause.`;
// Only show refund info if rental had a cost
if (rental.totalAmount > 0) {
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>
</div>
<div style="text-align: center">
<a href="${browseUrl}" class="button">Browse Other Items</a>
</div>
`;
} else {
additionalInfo = `
<div class="info-box">
<p>This rental has been cancelled by the owner. We apologize for any inconvenience.</p>
</div>
<div style="text-align: center">
<a href="${browseUrl}" class="button">Browse Other Items</a>
</div>
`;
}
} 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 <strong>${itemName}</strong>.`;
additionalInfo = `
<div class="info-box">
<p><strong>Your item is now available</strong></p>
<p>Your item is now available for other renters to book for these dates.</p>
</div>
`;
}
// 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 = `
<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>Reason:</strong> ${refundInfo.reason}</p>
<p><strong>Processing Time:</strong> Refunds typically appear within 5-10 business days.</p>
</div>
`;
} else {
refundSection = `
<h2>Refund Information</h2>
<div class="warning-box">
<p><strong>No Refund Available</strong></p>
<p>${refundInfo.reason}</p>
</div>
`;
}
}
// Send confirmation email to canceller
try {
const confirmationVariables = {
recipientName: confirmationRecipientName,
itemName: itemName,
startDate: startDate,
endDate: endDate,
cancelledAt: cancelledAt,
refundSection: refundSection,
};
const confirmationHtml = await this.templateManager.renderTemplate(
"rentalCancellationConfirmationToUser",
confirmationVariables
);
const confirmationResult = await this.emailClient.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 = await this.templateManager.renderTemplate(
"rentalCancellationNotificationToUser",
notificationVariables
);
const notificationResult = await this.emailClient.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;
}
/**
* Send rental completion emails to both owner and renter
* @param {Object} owner - Owner user object
* @param {string} owner.email - Owner's email address
* @param {string} owner.firstName - Owner's first name
* @param {string} owner.lastName - Owner's last name
* @param {string} owner.stripeConnectedAccountId - Owner's Stripe account ID
* @param {Object} renter - Renter user object
* @param {string} renter.email - Renter's email address
* @param {string} renter.firstName - Renter's first name
* @param {string} renter.lastName - Renter's last name
* @param {Object} rental - Rental object with all details
* @param {number} rental.id - Rental ID
* @param {Object} rental.item - Item object
* @param {string} rental.item.name - Item name
* @param {string} rental.startDateTime - Rental start date
* @param {string} rental.endDateTime - Rental end date
* @param {string} rental.actualReturnDateTime - Actual return timestamp
* @param {string} rental.itemReviewSubmittedAt - Review submission timestamp
* @param {string} rental.totalAmount - Total rental amount
* @param {string} rental.payoutAmount - Owner's payout amount
* @param {string} rental.platformFee - Platform fee amount
* @returns {Promise<{renterEmailSent: boolean, ownerEmailSent: boolean}>}
*/
async sendRentalCompletionEmails(owner, renter, rental) {
if (!this.initialized) {
await this.initialize();
}
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const results = {
renterEmailSent: false,
ownerEmailSent: false,
};
try {
// 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 = `
<h2>Share Your Experience</h2>
<div class="info-box">
<p><strong>Help the community by leaving a review!</strong></p>
<p>Your feedback helps other renters make informed decisions and supports quality listings on Village Share.</p>
<ul>
<li>How was the item's condition?</li>
<li>Was the owner responsive and helpful?</li>
<li>Would you rent this item again?</li>
</ul>
</div>
<p style="text-align: center;">
<a href="${frontendUrl}/renting?rentalId=${rental.id}&action=review" class="button">Leave a Review</a>
</p>
`;
} else {
reviewSection = `
<div class="success-box">
<p><strong>✓ Thank You for Your Review!</strong></p>
<p>Your feedback has been submitted and helps strengthen the Village Share community.</p>
</div>
`;
}
// 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 = await this.templateManager.renderTemplate(
"rentalCompletionThankYouToRenter",
renterVariables
);
const renterResult = await this.emailClient.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) {
logger.error("Failed to send rental completion email to renter", {
error: emailError.message,
stack: emailError.stack,
renterEmail: renter.email,
rentalId: rental.id,
});
}
// 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 = `
<h2>Your Earnings</h2>
<table class="info-table">
<tr>
<th>Total Rental Amount</th>
<td>$${totalAmount.toFixed(2)}</td>
</tr>
<tr>
<th>Community Upkeep Fee (10%)</th>
<td>-$${platformFee.toFixed(2)}</td>
</tr>
<tr>
<th>Your Payout</th>
<td class="highlight">$${payoutAmount.toFixed(2)}</td>
</tr>
</table>
<p style="font-size: 14px; color: #6c757d;">
Your earnings are transferred immediately when the rental is marked complete. Funds typically reach your bank within 2-7 business days.
</p>
`;
}
// Build Stripe section for owner
let stripeSection = "";
if (!hasStripeAccount && isPaidRental) {
// Show Stripe setup reminder for paid rentals
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>
</div>
<h2>Set Up Earnings to Get Paid</h2>
<div class="info-box">
<p><strong>Why set up now?</strong></p>
<ul>
<li><strong>Automatic payouts</strong> when the rental period ends</li>
<li><strong>Secure transfers</strong> directly to your bank account</li>
<li><strong>Track all earnings</strong> in one dashboard</li>
<li><strong>Fast deposits</strong> (typically 2-7 business days)</li>
</ul>
<p>Setup only takes about 5 minutes and you only need to do it once.</p>
</div>
<p style="text-align: center;">
<a href="${frontendUrl}/earnings" class="button">Set Up Earnings Account Now</a>
</p>
<p style="text-align: center; font-size: 14px; color: #856404;">
<strong>Important:</strong> Without earnings setup, you won't receive payouts automatically.
</p>
`;
} else if (hasStripeAccount && isPaidRental) {
stripeSection = `
<div class="success-box">
<p><strong>✓ Payout Initiated</strong></p>
<p>Your earnings of <strong>$${payoutAmount.toFixed(
2
)}</strong> have been transferred to your Stripe account.</p>
<p style="font-size: 14px; margin-top: 10px;">Funds typically reach your bank within 2-7 business days.</p>
<p><a href="${frontendUrl}/earnings" style="color: #155724; text-decoration: underline;">View your earnings dashboard →</a></p>
</div>
`;
}
// 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,
owningUrl: `${frontendUrl}/owning`,
};
const ownerHtmlContent = await this.templateManager.renderTemplate(
"rentalCompletionCongratsToOwner",
ownerVariables
);
const ownerResult = await this.emailClient.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) {
logger.error("Failed to send rental completion email to owner", {
error: emailError.message,
stack: emailError.stack,
ownerEmail: owner.email,
rentalId: rental.id,
});
}
} catch (error) {
logger.error("Error sending rental completion emails", {
error: error.message,
stack: error.stack,
rentalId: rental?.id,
});
}
return results;
}
/**
* Send payout received email to owner
* @param {Object} owner - Owner user object
* @param {string} owner.email - Owner's email address
* @param {string} owner.firstName - Owner's first name
* @param {Object} rental - Rental object with all details
* @param {Object} rental.item - Item object
* @param {string} rental.item.name - Item name
* @param {string} rental.startDateTime - Rental start date
* @param {string} rental.endDateTime - Rental end date
* @param {string} rental.totalAmount - Total rental amount
* @param {string} rental.platformFee - Platform fee amount
* @param {string} rental.payoutAmount - Owner's payout amount
* @param {string} rental.stripeTransferId - Stripe transfer ID
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
*/
async sendPayoutReceivedEmail(owner, rental) {
if (!this.initialized) {
await this.initialize();
}
try {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const earningsDashboardUrl = `${frontendUrl}/earnings`;
// 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 = await this.templateManager.renderTemplate(
"payoutReceivedToOwner",
variables
);
return await this.emailClient.sendEmail(
owner.email,
`Earnings Received - $${payoutAmount.toFixed(2)} for ${
rental.item?.name || "Your Item"
}`,
htmlContent
);
} catch (error) {
console.error("Failed to send payout received email:", error);
return { success: false, error: error.message };
}
}
/**
* Send authentication required email to renter when 3DS verification is needed
* This is sent when the owner approves a rental but the renter's bank requires
* additional verification (3D Secure) to complete the payment.
*
* @param {string} email - Renter's email address
* @param {Object} data - Email data
* @param {string} data.renterName - Renter's first name
* @param {string} data.itemName - Name of the item being rented
* @param {string} data.ownerName - Owner's first name
* @param {number} data.amount - Total rental amount
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
*/
async sendAuthenticationRequiredEmail(email, data) {
if (!this.initialized) {
await this.initialize();
}
try {
const { renterName, itemName, ownerName, amount } = data;
const variables = {
renterName: renterName || "there",
itemName: itemName || "the item",
ownerName: ownerName || "The owner",
amount: typeof amount === "number" ? amount.toFixed(2) : "0.00",
};
const htmlContent = await this.templateManager.renderTemplate(
"authenticationRequiredToRenter",
variables
);
return await this.emailClient.sendEmail(
email,
`Action Required: Complete payment for ${itemName}`,
htmlContent
);
} catch (error) {
console.error("Failed to send authentication required email:", error);
return { success: false, error: error.message };
}
}
}
module.exports = RentalFlowEmailService;