1215 lines
45 KiB
JavaScript
1215 lines
45 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 };
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = RentalFlowEmailService;
|