payment confirmation for renter after rental request approval, first listing celebration email, removed burstprotection for google places autocomplete, renamed email templates
This commit is contained in:
@@ -33,20 +33,24 @@ class EmailService {
|
||||
|
||||
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",
|
||||
"conditionCheckReminderToUser.html",
|
||||
"rentalConfirmationToUser.html",
|
||||
"emailVerificationToUser.html",
|
||||
"passwordResetToUser.html",
|
||||
"passwordChangedToUser.html",
|
||||
"lateReturnToCS.html",
|
||||
"damageReportToCS.html",
|
||||
"lostItemToCS.html",
|
||||
"rentalRequestToOwner.html",
|
||||
"rentalRequestConfirmationToRenter.html",
|
||||
"rentalCancellationConfirmationToUser.html",
|
||||
"rentalCancellationNotificationToUser.html",
|
||||
"rentalDeclinedToRenter.html",
|
||||
"rentalApprovalConfirmationToOwner.html",
|
||||
"rentalCompletionThankYouToRenter.html",
|
||||
"rentalCompletionCongratsToOwner.html",
|
||||
"payoutReceivedToOwner.html",
|
||||
"firstListingCelebrationToOwner.html",
|
||||
];
|
||||
|
||||
for (const templateFile of templateFiles) {
|
||||
@@ -222,7 +226,7 @@ class EmailService {
|
||||
`;
|
||||
|
||||
const templates = {
|
||||
conditionCheckReminder: baseTemplate.replace(
|
||||
conditionCheckReminderToUser: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<h2>{{title}}</h2>
|
||||
@@ -233,7 +237,7 @@ class EmailService {
|
||||
`
|
||||
),
|
||||
|
||||
rentalConfirmation: baseTemplate.replace(
|
||||
rentalConfirmationToUser: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{recipientName}},</p>
|
||||
@@ -245,7 +249,7 @@ class EmailService {
|
||||
`
|
||||
),
|
||||
|
||||
emailVerification: baseTemplate.replace(
|
||||
emailVerificationToUser: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{recipientName}},</p>
|
||||
@@ -257,7 +261,7 @@ class EmailService {
|
||||
`
|
||||
),
|
||||
|
||||
passwordReset: baseTemplate.replace(
|
||||
passwordResetToUser: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{recipientName}},</p>
|
||||
@@ -270,7 +274,7 @@ class EmailService {
|
||||
`
|
||||
),
|
||||
|
||||
passwordChanged: baseTemplate.replace(
|
||||
passwordChangedToUser: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{recipientName}},</p>
|
||||
@@ -282,7 +286,7 @@ class EmailService {
|
||||
`
|
||||
),
|
||||
|
||||
rentalRequest: baseTemplate.replace(
|
||||
rentalRequestToOwner: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{ownerName}},</p>
|
||||
@@ -298,7 +302,7 @@ class EmailService {
|
||||
`
|
||||
),
|
||||
|
||||
rentalRequestConfirmation: baseTemplate.replace(
|
||||
rentalRequestConfirmationToRenter: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{renterName}},</p>
|
||||
@@ -314,7 +318,7 @@ class EmailService {
|
||||
`
|
||||
),
|
||||
|
||||
rentalCancellationConfirmation: baseTemplate.replace(
|
||||
rentalCancellationConfirmationToUser: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{recipientName}},</p>
|
||||
@@ -328,7 +332,7 @@ class EmailService {
|
||||
`
|
||||
),
|
||||
|
||||
rentalCancellationNotification: baseTemplate.replace(
|
||||
rentalCancellationNotificationToUser: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{recipientName}},</p>
|
||||
@@ -343,7 +347,7 @@ class EmailService {
|
||||
`
|
||||
),
|
||||
|
||||
payoutReceived: baseTemplate.replace(
|
||||
payoutReceivedToOwner: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{ownerName}},</p>
|
||||
@@ -363,7 +367,7 @@ class EmailService {
|
||||
`
|
||||
),
|
||||
|
||||
rentalDeclined: baseTemplate.replace(
|
||||
rentalDeclinedToRenter: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{renterName}},</p>
|
||||
@@ -384,6 +388,60 @@ class EmailService {
|
||||
<p>If you have any questions or concerns, please don't hesitate to contact our support team.</p>
|
||||
`
|
||||
),
|
||||
|
||||
rentalApprovalConfirmationToOwner: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{ownerName}},</p>
|
||||
<h2>You've Approved the Rental Request!</h2>
|
||||
<p>You've successfully approved the rental request for <strong>{{itemName}}</strong>.</p>
|
||||
<h3>Rental Details</h3>
|
||||
<p><strong>Item:</strong> {{itemName}}</p>
|
||||
<p><strong>Renter:</strong> {{renterName}}</p>
|
||||
<p><strong>Start Date:</strong> {{startDate}}</p>
|
||||
<p><strong>End Date:</strong> {{endDate}}</p>
|
||||
<p><strong>Your Earnings:</strong> \${{payoutAmount}}</p>
|
||||
{{stripeSection}}
|
||||
<h3>What's Next?</h3>
|
||||
<ul>
|
||||
<li>Coordinate with the renter on pickup details</li>
|
||||
<li>Take photos of the item's condition before handoff</li>
|
||||
<li>Provide any care instructions or usage tips</li>
|
||||
</ul>
|
||||
<p><a href="{{rentalDetailsUrl}}" class="button">View Rental Details</a></p>
|
||||
`
|
||||
),
|
||||
|
||||
rentalCompletionThankYouToRenter: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{renterName}},</p>
|
||||
<h2>Thank You for Returning On Time!</h2>
|
||||
<p>You've successfully returned <strong>{{itemName}}</strong> on time. On-time returns like yours help build trust in the RentAll community!</p>
|
||||
<h3>Rental Summary</h3>
|
||||
<p><strong>Item:</strong> {{itemName}}</p>
|
||||
<p><strong>Rental Period:</strong> {{startDate}} to {{endDate}}</p>
|
||||
<p><strong>Returned On:</strong> {{returnedDate}}</p>
|
||||
{{reviewSection}}
|
||||
<p><a href="{{browseItemsUrl}}" class="button">Browse Available Items</a></p>
|
||||
`
|
||||
),
|
||||
|
||||
rentalCompletionCongratsToOwner: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{ownerName}},</p>
|
||||
<h2>Congratulations on Completing a Rental!</h2>
|
||||
<p><strong>{{itemName}}</strong> has been successfully returned on time. Great job!</p>
|
||||
<h3>Rental Summary</h3>
|
||||
<p><strong>Item:</strong> {{itemName}}</p>
|
||||
<p><strong>Renter:</strong> {{renterName}}</p>
|
||||
<p><strong>Rental Period:</strong> {{startDate}} to {{endDate}}</p>
|
||||
{{earningsSection}}
|
||||
{{stripeSection}}
|
||||
<p><a href="{{myListingsUrl}}" class="button">View My Listings</a></p>
|
||||
`
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -409,7 +467,7 @@ class EmailService {
|
||||
};
|
||||
|
||||
const htmlContent = this.renderTemplate(
|
||||
"conditionCheckReminder",
|
||||
"conditionCheckReminderToUser",
|
||||
variables
|
||||
);
|
||||
|
||||
@@ -424,7 +482,8 @@ class EmailService {
|
||||
userEmail,
|
||||
notification,
|
||||
rental,
|
||||
recipientName = null
|
||||
recipientName = null,
|
||||
isRenter = false
|
||||
) {
|
||||
const itemName = rental?.item?.name || "Unknown Item";
|
||||
|
||||
@@ -439,9 +498,77 @@ class EmailService {
|
||||
endDate: rental?.endDateTime
|
||||
? new Date(rental.endDateTime).toLocaleDateString()
|
||||
: "Not specified",
|
||||
isRenter: isRenter,
|
||||
};
|
||||
|
||||
const htmlContent = this.renderTemplate("rentalConfirmation", variables);
|
||||
// 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 = this.renderTemplate("rentalConfirmationToUser", variables);
|
||||
|
||||
// Use clear, transactional subject line with item name
|
||||
const subject = `Rental Confirmation - ${itemName}`;
|
||||
@@ -458,7 +585,7 @@ class EmailService {
|
||||
verificationUrl: verificationUrl,
|
||||
};
|
||||
|
||||
const htmlContent = this.renderTemplate("emailVerification", variables);
|
||||
const htmlContent = this.renderTemplate("emailVerificationToUser", variables);
|
||||
|
||||
return await this.sendEmail(
|
||||
user.email,
|
||||
@@ -476,7 +603,7 @@ class EmailService {
|
||||
resetUrl: resetUrl,
|
||||
};
|
||||
|
||||
const htmlContent = this.renderTemplate("passwordReset", variables);
|
||||
const htmlContent = this.renderTemplate("passwordResetToUser", variables);
|
||||
|
||||
return await this.sendEmail(
|
||||
user.email,
|
||||
@@ -497,7 +624,7 @@ class EmailService {
|
||||
timestamp: timestamp,
|
||||
};
|
||||
|
||||
const htmlContent = this.renderTemplate("passwordChanged", variables);
|
||||
const htmlContent = this.renderTemplate("passwordChangedToUser", variables);
|
||||
|
||||
return await this.sendEmail(
|
||||
user.email,
|
||||
@@ -554,7 +681,7 @@ class EmailService {
|
||||
approveUrl: approveUrl,
|
||||
};
|
||||
|
||||
const htmlContent = this.renderTemplate("rentalRequest", variables);
|
||||
const htmlContent = this.renderTemplate("rentalRequestToOwner", variables);
|
||||
|
||||
return await this.sendEmail(
|
||||
owner.email,
|
||||
@@ -608,7 +735,7 @@ class EmailService {
|
||||
};
|
||||
|
||||
const htmlContent = this.renderTemplate(
|
||||
"rentalRequestConfirmation",
|
||||
"rentalRequestConfirmationToRenter",
|
||||
variables
|
||||
);
|
||||
|
||||
@@ -678,7 +805,7 @@ class EmailService {
|
||||
};
|
||||
|
||||
const htmlContent = this.renderTemplate(
|
||||
"rentalDeclined",
|
||||
"rentalDeclinedToRenter",
|
||||
variables
|
||||
);
|
||||
|
||||
@@ -730,7 +857,7 @@ class EmailService {
|
||||
earningsDashboardUrl: earningsDashboardUrl,
|
||||
};
|
||||
|
||||
const htmlContent = this.renderTemplate("payoutReceived", variables);
|
||||
const htmlContent = this.renderTemplate("payoutReceivedToOwner", variables);
|
||||
|
||||
return await this.sendEmail(
|
||||
owner.email,
|
||||
@@ -874,7 +1001,7 @@ class EmailService {
|
||||
};
|
||||
|
||||
const confirmationHtml = this.renderTemplate(
|
||||
"rentalCancellationConfirmation",
|
||||
"rentalCancellationConfirmationToUser",
|
||||
confirmationVariables
|
||||
);
|
||||
|
||||
@@ -910,7 +1037,7 @@ class EmailService {
|
||||
};
|
||||
|
||||
const notificationHtml = this.renderTemplate(
|
||||
"rentalCancellationNotification",
|
||||
"rentalCancellationNotificationToUser",
|
||||
notificationVariables
|
||||
);
|
||||
|
||||
@@ -965,7 +1092,7 @@ class EmailService {
|
||||
await this.sendTemplateEmail(
|
||||
process.env.CUSTOMER_SUPPORT_EMAIL,
|
||||
"Late Return Detected - Action Required",
|
||||
"lateReturnCS",
|
||||
"lateReturnToCS",
|
||||
{
|
||||
rentalId: rental.id,
|
||||
itemName: rental.item.name,
|
||||
@@ -1027,7 +1154,7 @@ class EmailService {
|
||||
await this.sendTemplateEmail(
|
||||
process.env.CUSTOMER_SUPPORT_EMAIL,
|
||||
"Damage Report Filed - Action Required",
|
||||
"damageReportCS",
|
||||
"damageReportToCS",
|
||||
{
|
||||
rentalId: rental.id,
|
||||
itemName: rental.item.name,
|
||||
@@ -1086,7 +1213,7 @@ class EmailService {
|
||||
await this.sendTemplateEmail(
|
||||
process.env.CUSTOMER_SUPPORT_EMAIL,
|
||||
"Lost Item Claim Filed - Action Required",
|
||||
"lostItemCS",
|
||||
"lostItemToCS",
|
||||
{
|
||||
rentalId: rental.id,
|
||||
itemName: rental.item.name,
|
||||
@@ -1153,7 +1280,8 @@ class EmailService {
|
||||
owner.email,
|
||||
ownerNotification,
|
||||
rental,
|
||||
owner.firstName
|
||||
owner.firstName,
|
||||
false // isRenter = false for owner
|
||||
);
|
||||
if (ownerResult.success) {
|
||||
console.log(
|
||||
@@ -1181,7 +1309,8 @@ class EmailService {
|
||||
renter.email,
|
||||
renterNotification,
|
||||
rental,
|
||||
renter.firstName
|
||||
renter.firstName,
|
||||
true // isRenter = true for renter (enables payment receipt)
|
||||
);
|
||||
if (renterResult.success) {
|
||||
console.log(
|
||||
@@ -1210,6 +1339,391 @@ class EmailService {
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async sendFirstListingCelebrationEmail(owner, item) {
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
|
||||
const variables = {
|
||||
ownerName: owner.firstName || "there",
|
||||
itemName: item.name,
|
||||
itemId: item.id,
|
||||
viewItemUrl: `${frontendUrl}/items/${item.id}`,
|
||||
};
|
||||
|
||||
const htmlContent = this.renderTemplate(
|
||||
"firstListingCelebrationToOwner",
|
||||
variables
|
||||
);
|
||||
|
||||
const subject = `🎉 Congratulations! Your first item is live on RentAll`;
|
||||
|
||||
return await this.sendEmail(owner.email, subject, htmlContent);
|
||||
}
|
||||
|
||||
async sendRentalApprovalConfirmationEmail(rental) {
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
|
||||
// Fetch owner details
|
||||
const owner = await User.findByPk(rental.ownerId, {
|
||||
attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"],
|
||||
});
|
||||
|
||||
// Fetch renter details
|
||||
const renter = await User.findByPk(rental.renterId, {
|
||||
attributes: ["firstName", "lastName"],
|
||||
});
|
||||
|
||||
if (!owner || !renter) {
|
||||
console.error(
|
||||
"Owner or renter not found for rental approval confirmation email"
|
||||
);
|
||||
return { success: false, error: "User not found" };
|
||||
}
|
||||
|
||||
// Determine if Stripe setup is needed
|
||||
const hasStripeAccount = !!owner.stripeConnectedAccountId;
|
||||
const totalAmount = parseFloat(rental.totalAmount) || 0;
|
||||
const payoutAmount = parseFloat(rental.payoutAmount) || 0;
|
||||
const platformFee = parseFloat(rental.platformFee) || 0;
|
||||
|
||||
// Build payment message
|
||||
const isPaidRental = totalAmount > 0;
|
||||
let paymentMessage = "";
|
||||
if (isPaidRental) {
|
||||
paymentMessage = "their payment has been processed successfully.";
|
||||
} else {
|
||||
paymentMessage = "this is a free rental (no payment required).";
|
||||
}
|
||||
|
||||
// Build earnings section (only for paid rentals)
|
||||
let earningsSection = "";
|
||||
if (isPaidRental) {
|
||||
earningsSection = `
|
||||
<h2>Your Earnings</h2>
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<th>Total Rental Amount</th>
|
||||
<td>\$${totalAmount.toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Platform Fee (20%)</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-3 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}/my-listings?rentalId=${rental.id}`,
|
||||
};
|
||||
|
||||
const htmlContent = this.renderTemplate(
|
||||
"rentalApprovalConfirmationToOwner",
|
||||
variables
|
||||
);
|
||||
|
||||
const subject = `Rental Approved - ${rental.item?.name || "Your Item"}`;
|
||||
|
||||
return await this.sendEmail(owner.email, subject, htmlContent);
|
||||
}
|
||||
|
||||
async sendRentalCompletionEmails(rental) {
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
const results = {
|
||||
renterEmailSent: false,
|
||||
ownerEmailSent: false,
|
||||
};
|
||||
|
||||
try {
|
||||
// Fetch owner details with Stripe info
|
||||
const owner = await User.findByPk(rental.ownerId, {
|
||||
attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"],
|
||||
});
|
||||
|
||||
// Fetch renter details
|
||||
const renter = await User.findByPk(rental.renterId, {
|
||||
attributes: ["email", "firstName", "lastName"],
|
||||
});
|
||||
|
||||
if (!owner || !renter) {
|
||||
console.error(
|
||||
"Owner or renter not found for rental completion emails"
|
||||
);
|
||||
return { success: false, error: "User not found" };
|
||||
}
|
||||
|
||||
// Format dates
|
||||
const startDate = rental.startDateTime
|
||||
? new Date(rental.startDateTime).toLocaleString("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
})
|
||||
: "Not specified";
|
||||
const endDate = rental.endDateTime
|
||||
? new Date(rental.endDateTime).toLocaleString("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
})
|
||||
: "Not specified";
|
||||
const returnedDate = rental.actualReturnDateTime
|
||||
? new Date(rental.actualReturnDateTime).toLocaleString("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
})
|
||||
: endDate;
|
||||
|
||||
// Check if renter has already submitted a review
|
||||
const hasReviewed = !!rental.itemReviewSubmittedAt;
|
||||
|
||||
// Build review section for renter email
|
||||
let reviewSection = "";
|
||||
if (!hasReviewed) {
|
||||
reviewSection = `
|
||||
<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 RentAll.</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}/my-rentals?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 RentAll 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 = this.renderTemplate(
|
||||
"rentalCompletionThankYouToRenter",
|
||||
renterVariables
|
||||
);
|
||||
|
||||
const renterResult = await this.sendEmail(
|
||||
renter.email,
|
||||
`Thank You for Returning "${rental.item?.name || "Item"}" On Time!`,
|
||||
renterHtmlContent
|
||||
);
|
||||
|
||||
if (renterResult.success) {
|
||||
console.log(
|
||||
`Rental completion thank you email sent to renter: ${renter.email}`
|
||||
);
|
||||
results.renterEmailSent = true;
|
||||
} else {
|
||||
console.error(
|
||||
`Failed to send rental completion email to renter (${renter.email}):`,
|
||||
renterResult.error
|
||||
);
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.error(
|
||||
`Failed to send rental completion email to renter (${renter.email}):`,
|
||||
emailError.message
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare owner email
|
||||
const hasStripeAccount = !!owner.stripeConnectedAccountId;
|
||||
const totalAmount = parseFloat(rental.totalAmount) || 0;
|
||||
const payoutAmount = parseFloat(rental.payoutAmount) || 0;
|
||||
const platformFee = parseFloat(rental.platformFee) || 0;
|
||||
const isPaidRental = totalAmount > 0;
|
||||
|
||||
// Build earnings section for owner (only for paid rentals)
|
||||
let earningsSection = "";
|
||||
if (isPaidRental) {
|
||||
earningsSection = `
|
||||
<h2>Your Earnings</h2>
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<th>Total Rental Amount</th>
|
||||
<td>\$${totalAmount.toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Platform Fee (20%)</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 will be automatically transferred to your account when the rental period ends and any dispute windows close.
|
||||
</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-3 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>✓ Earnings Account Active</strong></p>
|
||||
<p>Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when the rental period ends.</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,
|
||||
myListingsUrl: `${frontendUrl}/my-listings`,
|
||||
};
|
||||
|
||||
const ownerHtmlContent = this.renderTemplate(
|
||||
"rentalCompletionCongratsToOwner",
|
||||
ownerVariables
|
||||
);
|
||||
|
||||
const ownerResult = await this.sendEmail(
|
||||
owner.email,
|
||||
`Rental Complete - ${rental.item?.name || "Your Item"}`,
|
||||
ownerHtmlContent
|
||||
);
|
||||
|
||||
if (ownerResult.success) {
|
||||
console.log(
|
||||
`Rental completion congratulations email sent to owner: ${owner.email}`
|
||||
);
|
||||
results.ownerEmailSent = true;
|
||||
} else {
|
||||
console.error(
|
||||
`Failed to send rental completion email to owner (${owner.email}):`,
|
||||
ownerResult.error
|
||||
);
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.error(
|
||||
`Failed to send rental completion email to owner (${owner.email}):`,
|
||||
emailError.message
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending rental completion emails:", error);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new EmailService();
|
||||
|
||||
@@ -123,14 +123,48 @@ class StripeService {
|
||||
payment_method: paymentMethodId,
|
||||
customer: customerId, // Include customer ID
|
||||
confirm: true, // Automatically confirm the payment
|
||||
off_session: true, // Indicate this is an off-session payment
|
||||
return_url: `${process.env.FRONTEND_URL || 'http://localhost:3000'}/payment-complete`,
|
||||
metadata,
|
||||
expand: ['charges.data.payment_method_details'], // Expand to get payment method details
|
||||
});
|
||||
|
||||
// Extract payment method details from charges
|
||||
const charge = paymentIntent.charges?.data?.[0];
|
||||
const paymentMethodDetails = charge?.payment_method_details;
|
||||
|
||||
// Build payment method info object
|
||||
let paymentMethod = null;
|
||||
if (paymentMethodDetails) {
|
||||
const type = paymentMethodDetails.type;
|
||||
if (type === 'card') {
|
||||
paymentMethod = {
|
||||
type: 'card',
|
||||
brand: paymentMethodDetails.card?.brand || 'card',
|
||||
last4: paymentMethodDetails.card?.last4 || '****',
|
||||
};
|
||||
} else if (type === 'us_bank_account') {
|
||||
paymentMethod = {
|
||||
type: 'bank',
|
||||
brand: 'bank_account',
|
||||
last4: paymentMethodDetails.us_bank_account?.last4 || '****',
|
||||
};
|
||||
} else {
|
||||
paymentMethod = {
|
||||
type: type || 'unknown',
|
||||
brand: type || 'payment',
|
||||
last4: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
paymentIntentId: paymentIntent.id,
|
||||
status: paymentIntent.status,
|
||||
clientSecret: paymentIntent.client_secret,
|
||||
paymentMethod: paymentMethod,
|
||||
chargedAt: new Date(paymentIntent.created * 1000), // Convert Unix timestamp to Date
|
||||
amountCharged: amount, // Original amount in dollars
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error charging payment method:", error);
|
||||
@@ -162,9 +196,6 @@ class StripeService {
|
||||
mode: 'setup',
|
||||
ui_mode: 'embedded',
|
||||
redirect_on_completion: 'never',
|
||||
setup_intent_data: {
|
||||
usage: 'off_session'
|
||||
},
|
||||
metadata: {
|
||||
type: 'payment_method_setup',
|
||||
...metadata
|
||||
|
||||
Reference in New Issue
Block a user