From d1cb857aa7fa5c8ed79bf60eead667c93e3545fa Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:23:41 -0400 Subject: [PATCH] payment confirmation for renter after rental request approval, first listing celebration email, removed burstprotection for google places autocomplete, renamed email templates --- backend/models/Rental.js | 9 + backend/routes/items.js | 25 +- backend/routes/maps.js | 2 - backend/routes/rentals.js | 143 ++++- backend/services/emailService.js | 598 ++++++++++++++++-- backend/services/stripeService.js | 37 +- ...html => conditionCheckReminderToUser.html} | 0 ...ageReportCS.html => damageReportToCS.html} | 0 ...tion.html => emailVerificationToUser.html} | 0 .../firstListingCelebrationToOwner.html | 314 +++++++++ ...{lateReturnCS.html => lateReturnToCS.html} | 0 .../{lostItemCS.html => lostItemToCS.html} | 0 ...hanged.html => passwordChangedToUser.html} | 0 ...ordReset.html => passwordResetToUser.html} | 0 ...ceived.html => payoutReceivedToOwner.html} | 0 .../rentalApprovalConfirmationToOwner.html | 357 +++++++++++ ...rentalCancellationConfirmationToUser.html} | 0 ...rentalCancellationNotificationToUser.html} | 0 .../rentalCompletionCongratsToOwner.html | 372 +++++++++++ .../rentalCompletionThankYouToRenter.html | 314 +++++++++ ...ion.html => rentalConfirmationToUser.html} | 2 + ...lined.html => rentalDeclinedToRenter.html} | 0 ...=> rentalRequestConfirmationToRenter.html} | 0 ...Request.html => rentalRequestToOwner.html} | 0 frontend/src/services/placesService.ts | 51 +- 25 files changed, 2171 insertions(+), 53 deletions(-) rename backend/templates/emails/{conditionCheckReminder.html => conditionCheckReminderToUser.html} (100%) rename backend/templates/emails/{damageReportCS.html => damageReportToCS.html} (100%) rename backend/templates/emails/{emailVerification.html => emailVerificationToUser.html} (100%) create mode 100644 backend/templates/emails/firstListingCelebrationToOwner.html rename backend/templates/emails/{lateReturnCS.html => lateReturnToCS.html} (100%) rename backend/templates/emails/{lostItemCS.html => lostItemToCS.html} (100%) rename backend/templates/emails/{passwordChanged.html => passwordChangedToUser.html} (100%) rename backend/templates/emails/{passwordReset.html => passwordResetToUser.html} (100%) rename backend/templates/emails/{payoutReceived.html => payoutReceivedToOwner.html} (100%) create mode 100644 backend/templates/emails/rentalApprovalConfirmationToOwner.html rename backend/templates/emails/{rentalCancellationConfirmation.html => rentalCancellationConfirmationToUser.html} (100%) rename backend/templates/emails/{rentalCancellationNotification.html => rentalCancellationNotificationToUser.html} (100%) create mode 100644 backend/templates/emails/rentalCompletionCongratsToOwner.html create mode 100644 backend/templates/emails/rentalCompletionThankYouToRenter.html rename backend/templates/emails/{rentalConfirmation.html => rentalConfirmationToUser.html} (99%) rename backend/templates/emails/{rentalDeclined.html => rentalDeclinedToRenter.html} (100%) rename backend/templates/emails/{rentalRequestConfirmation.html => rentalRequestConfirmationToRenter.html} (100%) rename backend/templates/emails/{rentalRequest.html => rentalRequestToOwner.html} (100%) diff --git a/backend/models/Rental.js b/backend/models/Rental.js index 27ebeb6..9008330 100644 --- a/backend/models/Rental.js +++ b/backend/models/Rental.js @@ -108,6 +108,15 @@ const Rental = sequelize.define("Rental", { stripePaymentIntentId: { type: DataTypes.STRING, }, + paymentMethodBrand: { + type: DataTypes.STRING, + }, + paymentMethodLast4: { + type: DataTypes.STRING, + }, + chargedAt: { + type: DataTypes.DATE, + }, deliveryMethod: { type: DataTypes.ENUM("pickup", "delivery"), defaultValue: "pickup", diff --git a/backend/routes/items.js b/backend/routes/items.js index 615dceb..3d5e259 100644 --- a/backend/routes/items.js +++ b/backend/routes/items.js @@ -225,16 +225,37 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => { { model: User, as: "owner", - attributes: ["id", "username", "firstName", "lastName"], + attributes: ["id", "username", "firstName", "lastName", "email", "stripeConnectedAccountId"], }, ], }); + // Check if this is the owner's first listing + const ownerItemCount = await Item.count({ + where: { ownerId: req.user.id } + }); + + // If first listing, send celebration email + if (ownerItemCount === 1) { + try { + const emailService = require("../services/emailService"); + await emailService.sendFirstListingCelebrationEmail( + itemWithOwner.owner, + itemWithOwner + ); + console.log(`First listing celebration email sent to owner ${req.user.id}`); + } catch (emailError) { + // Log but don't fail the item creation + console.error('Failed to send first listing celebration email:', emailError.message); + } + } + const reqLogger = logger.withRequestId(req.id); reqLogger.info("Item created", { itemId: item.id, ownerId: req.user.id, - itemName: req.body.name + itemName: req.body.name, + isFirstListing: ownerItemCount === 1 }); res.status(201).json(itemWithOwner); diff --git a/backend/routes/maps.js b/backend/routes/maps.js index f29b381..c54b2cb 100644 --- a/backend/routes/maps.js +++ b/backend/routes/maps.js @@ -69,7 +69,6 @@ const handleServiceError = (error, res, req) => { router.post( "/places/autocomplete", authenticateToken, - rateLimiter.burstProtection, rateLimiter.placesAutocomplete, validateInput, async (req, res) => { @@ -150,7 +149,6 @@ router.post( router.post( "/geocode", authenticateToken, - rateLimiter.burstProtection, rateLimiter.geocoding, validateInput, async (req, res) => { diff --git a/backend/routes/rentals.js b/backend/routes/rentals.js index 0afd412..a463a91 100644 --- a/backend/routes/rentals.js +++ b/backend/routes/rentals.js @@ -404,6 +404,9 @@ router.put("/:id/status", authenticateToken, async (req, res) => { status: "confirmed", paymentStatus: "paid", stripePaymentIntentId: paymentResult.paymentIntentId, + paymentMethodBrand: paymentResult.paymentMethod?.brand || null, + paymentMethodLast4: paymentResult.paymentMethod?.last4 || null, + chargedAt: paymentResult.chargedAt || new Date(), }); const updatedRental = await Rental.findByPk(rental.id, { @@ -423,7 +426,58 @@ router.put("/:id/status", authenticateToken, async (req, res) => { }); // Send confirmation emails - await emailService.sendRentalConfirmationEmails(updatedRental); + // Send approval confirmation to owner with Stripe reminder + try { + await emailService.sendRentalApprovalConfirmationEmail(updatedRental); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Rental approval confirmation sent to owner", { + rentalId: updatedRental.id, + ownerId: updatedRental.ownerId, + }); + } catch (emailError) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Failed to send rental approval confirmation email to owner", { + error: emailError.message, + rentalId: updatedRental.id, + ownerId: updatedRental.ownerId, + }); + } + + // Send rental confirmation to renter with payment receipt + try { + const renter = await User.findByPk(updatedRental.renterId, { + attributes: ["email", "firstName"], + }); + if (renter) { + const renterNotification = { + type: "rental_confirmed", + title: "Rental Confirmed", + message: `Your rental of "${updatedRental.item.name}" has been confirmed.`, + rentalId: updatedRental.id, + userId: updatedRental.renterId, + metadata: { rentalStart: updatedRental.startDateTime }, + }; + await emailService.sendRentalConfirmation( + renter.email, + renterNotification, + updatedRental, + renter.firstName, + true // isRenter = true to show payment receipt + ); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Rental confirmation sent to renter", { + rentalId: updatedRental.id, + renterId: updatedRental.renterId, + }); + } + } catch (emailError) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Failed to send rental confirmation email to renter", { + error: emailError.message, + rentalId: updatedRental.id, + renterId: updatedRental.renterId, + }); + } res.json(updatedRental); return; @@ -464,7 +518,58 @@ router.put("/:id/status", authenticateToken, async (req, res) => { }); // Send confirmation emails - await emailService.sendRentalConfirmationEmails(updatedRental); + // Send approval confirmation to owner (for free rentals, no Stripe reminder shown) + try { + await emailService.sendRentalApprovalConfirmationEmail(updatedRental); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Rental approval confirmation sent to owner", { + rentalId: updatedRental.id, + ownerId: updatedRental.ownerId, + }); + } catch (emailError) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Failed to send rental approval confirmation email to owner", { + error: emailError.message, + rentalId: updatedRental.id, + ownerId: updatedRental.ownerId, + }); + } + + // Send rental confirmation to renter + try { + const renter = await User.findByPk(updatedRental.renterId, { + attributes: ["email", "firstName"], + }); + if (renter) { + const renterNotification = { + type: "rental_confirmed", + title: "Rental Confirmed", + message: `Your rental of "${updatedRental.item.name}" has been confirmed.`, + rentalId: updatedRental.id, + userId: updatedRental.renterId, + metadata: { rentalStart: updatedRental.startDateTime }, + }; + await emailService.sendRentalConfirmation( + renter.email, + renterNotification, + updatedRental, + renter.firstName, + true // isRenter = true (for free rentals, shows "no payment required") + ); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Rental confirmation sent to renter", { + rentalId: updatedRental.id, + renterId: updatedRental.renterId, + }); + } + } catch (emailError) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Failed to send rental confirmation email to renter", { + error: emailError.message, + rentalId: updatedRental.id, + renterId: updatedRental.renterId, + }); + } res.json(updatedRental); return; @@ -958,6 +1063,40 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => { actualReturnDateTime: actualReturnDateTime || rental.endDateTime, notes: notes || null, }); + + // Fetch full rental details with associations for email + const rentalWithDetails = await Rental.findByPk(rentalId, { + include: [ + { model: Item, as: "item" }, + { + model: User, + as: "owner", + attributes: ["id", "username", "firstName", "lastName"], + }, + { + model: User, + as: "renter", + attributes: ["id", "username", "firstName", "lastName"], + }, + ], + }); + + // Send completion emails to both renter and owner + try { + await emailService.sendRentalCompletionEmails(rentalWithDetails); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Rental completion emails sent", { + rentalId, + ownerId: rental.ownerId, + renterId: rental.renterId, + }); + } catch (emailError) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Failed to send rental completion emails", { + error: emailError.message, + rentalId, + }); + } break; case "damaged": diff --git a/backend/services/emailService.js b/backend/services/emailService.js index a14f7d5..7c10a2e 100644 --- a/backend/services/emailService.js +++ b/backend/services/emailService.js @@ -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}}", `

{{title}}

@@ -233,7 +237,7 @@ class EmailService { ` ), - rentalConfirmation: baseTemplate.replace( + rentalConfirmationToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

@@ -245,7 +249,7 @@ class EmailService { ` ), - emailVerification: baseTemplate.replace( + emailVerificationToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

@@ -257,7 +261,7 @@ class EmailService { ` ), - passwordReset: baseTemplate.replace( + passwordResetToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

@@ -270,7 +274,7 @@ class EmailService { ` ), - passwordChanged: baseTemplate.replace( + passwordChangedToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

@@ -282,7 +286,7 @@ class EmailService { ` ), - rentalRequest: baseTemplate.replace( + rentalRequestToOwner: baseTemplate.replace( "{{content}}", `

Hi {{ownerName}},

@@ -298,7 +302,7 @@ class EmailService { ` ), - rentalRequestConfirmation: baseTemplate.replace( + rentalRequestConfirmationToRenter: baseTemplate.replace( "{{content}}", `

Hi {{renterName}},

@@ -314,7 +318,7 @@ class EmailService { ` ), - rentalCancellationConfirmation: baseTemplate.replace( + rentalCancellationConfirmationToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

@@ -328,7 +332,7 @@ class EmailService { ` ), - rentalCancellationNotification: baseTemplate.replace( + rentalCancellationNotificationToUser: baseTemplate.replace( "{{content}}", `

Hi {{recipientName}},

@@ -343,7 +347,7 @@ class EmailService { ` ), - payoutReceived: baseTemplate.replace( + payoutReceivedToOwner: baseTemplate.replace( "{{content}}", `

Hi {{ownerName}},

@@ -363,7 +367,7 @@ class EmailService { ` ), - rentalDeclined: baseTemplate.replace( + rentalDeclinedToRenter: baseTemplate.replace( "{{content}}", `

Hi {{renterName}},

@@ -384,6 +388,60 @@ class EmailService {

If you have any questions or concerns, please don't hesitate to contact our support team.

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

Hi {{ownerName}},

+

You've Approved the Rental Request!

+

You've successfully approved the rental request for {{itemName}}.

+

Rental Details

+

Item: {{itemName}}

+

Renter: {{renterName}}

+

Start Date: {{startDate}}

+

End Date: {{endDate}}

+

Your Earnings: \${{payoutAmount}}

+ {{stripeSection}} +

What's Next?

+ +

View Rental Details

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

Hi {{renterName}},

+

Thank You for Returning On Time!

+

You've successfully returned {{itemName}} on time. On-time returns like yours help build trust in the RentAll community!

+

Rental Summary

+

Item: {{itemName}}

+

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

+

Returned On: {{returnedDate}}

+ {{reviewSection}} +

Browse Available Items

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

Hi {{ownerName}},

+

Congratulations on Completing a Rental!

+

{{itemName}} has been successfully returned on time. Great job!

+

Rental Summary

+

Item: {{itemName}}

+

Renter: {{renterName}}

+

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

+ {{earningsSection}} + {{stripeSection}} +

View My Listings

+ ` + ), }; return ( @@ -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 = ` +

Payment Receipt

+
+
💳
+

Payment Successful

+

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

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

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

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

No Payment Required: This is a free rental.

+
+ `; + } + } + + variables.paymentSection = paymentSection; + + const htmlContent = this.renderTemplate("rentalConfirmationToUser", variables); // Use clear, transactional subject line with item name const subject = `Rental Confirmation - ${itemName}`; @@ -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 = ` +

Your Earnings

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

⚠️ Action Required: Set Up Your Earnings Account

+

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

+
+

Set Up Earnings to Get Paid

+
+

Why set up now?

+ +

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

+
+

+ Set Up Earnings Account Now +

+

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

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

✓ Earnings Account Active

+

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

+

View your earnings dashboard →

+
+ `; + } + + // Format delivery method for display + const deliveryMethodDisplay = rental.deliveryMethod === "delivery" ? "Delivery" : "Pickup"; + + const variables = { + ownerName: owner.firstName || "there", + itemName: rental.item?.name || "your item", + renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter", + startDate: rental.startDateTime + ? new Date(rental.startDateTime).toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "short", + }) + : "Not specified", + endDate: rental.endDateTime + ? new Date(rental.endDateTime).toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "short", + }) + : "Not specified", + deliveryMethod: deliveryMethodDisplay, + paymentMessage: paymentMessage, + earningsSection: earningsSection, + stripeSection: stripeSection, + rentalDetailsUrl: `${frontendUrl}/my-listings?rentalId=${rental.id}`, + }; + + const htmlContent = this.renderTemplate( + "rentalApprovalConfirmationToOwner", + variables + ); + + const subject = `Rental Approved - ${rental.item?.name || "Your Item"}`; + + return await this.sendEmail(owner.email, subject, htmlContent); + } + + async sendRentalCompletionEmails(rental) { + const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000"; + const results = { + renterEmailSent: false, + ownerEmailSent: false, + }; + + try { + // Fetch owner details with Stripe info + const owner = await User.findByPk(rental.ownerId, { + attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"], + }); + + // Fetch renter details + const renter = await User.findByPk(rental.renterId, { + attributes: ["email", "firstName", "lastName"], + }); + + if (!owner || !renter) { + console.error( + "Owner or renter not found for rental completion emails" + ); + return { success: false, error: "User not found" }; + } + + // Format dates + const startDate = rental.startDateTime + ? new Date(rental.startDateTime).toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "short", + }) + : "Not specified"; + const endDate = rental.endDateTime + ? new Date(rental.endDateTime).toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "short", + }) + : "Not specified"; + const returnedDate = rental.actualReturnDateTime + ? new Date(rental.actualReturnDateTime).toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "short", + }) + : endDate; + + // Check if renter has already submitted a review + const hasReviewed = !!rental.itemReviewSubmittedAt; + + // Build review section for renter email + let reviewSection = ""; + if (!hasReviewed) { + reviewSection = ` +

Share Your Experience

+
+

Help the community by leaving a review!

+

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

+ +
+

+ Leave a Review +

+ `; + } else { + reviewSection = ` +
+

✓ Thank You for Your Review!

+

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

+
+ `; + } + + // Send email to renter + try { + const renterVariables = { + renterName: renter.firstName || "there", + itemName: rental.item?.name || "the item", + ownerName: owner.firstName || "the owner", + startDate: startDate, + endDate: endDate, + returnedDate: returnedDate, + reviewSection: reviewSection, + browseItemsUrl: `${frontendUrl}/`, + }; + + const renterHtmlContent = this.renderTemplate( + "rentalCompletionThankYouToRenter", + renterVariables + ); + + const renterResult = await this.sendEmail( + renter.email, + `Thank You for Returning "${rental.item?.name || "Item"}" On Time!`, + renterHtmlContent + ); + + if (renterResult.success) { + console.log( + `Rental completion thank you email sent to renter: ${renter.email}` + ); + results.renterEmailSent = true; + } else { + console.error( + `Failed to send rental completion email to renter (${renter.email}):`, + renterResult.error + ); + } + } catch (emailError) { + console.error( + `Failed to send rental completion email to renter (${renter.email}):`, + emailError.message + ); + } + + // Prepare owner email + const hasStripeAccount = !!owner.stripeConnectedAccountId; + const totalAmount = parseFloat(rental.totalAmount) || 0; + const payoutAmount = parseFloat(rental.payoutAmount) || 0; + const platformFee = parseFloat(rental.platformFee) || 0; + const isPaidRental = totalAmount > 0; + + // Build earnings section for owner (only for paid rentals) + let earningsSection = ""; + if (isPaidRental) { + earningsSection = ` +

Your Earnings

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

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

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

⚠️ Action Required: Set Up Your Earnings Account

+

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

+
+

Set Up Earnings to Get Paid

+
+

Why set up now?

+ +

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

+
+

+ Set Up Earnings Account Now +

+

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

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

✓ Earnings Account Active

+

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

+

View your earnings dashboard →

+
+ `; + } + + // Send email to owner + try { + const ownerVariables = { + ownerName: owner.firstName || "there", + itemName: rental.item?.name || "your item", + renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter", + startDate: startDate, + endDate: endDate, + returnedDate: returnedDate, + earningsSection: earningsSection, + stripeSection: stripeSection, + myListingsUrl: `${frontendUrl}/my-listings`, + }; + + const ownerHtmlContent = this.renderTemplate( + "rentalCompletionCongratsToOwner", + ownerVariables + ); + + const ownerResult = await this.sendEmail( + owner.email, + `Rental Complete - ${rental.item?.name || "Your Item"}`, + ownerHtmlContent + ); + + if (ownerResult.success) { + console.log( + `Rental completion congratulations email sent to owner: ${owner.email}` + ); + results.ownerEmailSent = true; + } else { + console.error( + `Failed to send rental completion email to owner (${owner.email}):`, + ownerResult.error + ); + } + } catch (emailError) { + console.error( + `Failed to send rental completion email to owner (${owner.email}):`, + emailError.message + ); + } + } catch (error) { + console.error("Error sending rental completion emails:", error); + } + + return results; + } } module.exports = new EmailService(); diff --git a/backend/services/stripeService.js b/backend/services/stripeService.js index b68419f..9b53758 100644 --- a/backend/services/stripeService.js +++ b/backend/services/stripeService.js @@ -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 diff --git a/backend/templates/emails/conditionCheckReminder.html b/backend/templates/emails/conditionCheckReminderToUser.html similarity index 100% rename from backend/templates/emails/conditionCheckReminder.html rename to backend/templates/emails/conditionCheckReminderToUser.html diff --git a/backend/templates/emails/damageReportCS.html b/backend/templates/emails/damageReportToCS.html similarity index 100% rename from backend/templates/emails/damageReportCS.html rename to backend/templates/emails/damageReportToCS.html diff --git a/backend/templates/emails/emailVerification.html b/backend/templates/emails/emailVerificationToUser.html similarity index 100% rename from backend/templates/emails/emailVerification.html rename to backend/templates/emails/emailVerificationToUser.html diff --git a/backend/templates/emails/firstListingCelebrationToOwner.html b/backend/templates/emails/firstListingCelebrationToOwner.html new file mode 100644 index 0000000..6ed18ca --- /dev/null +++ b/backend/templates/emails/firstListingCelebrationToOwner.html @@ -0,0 +1,314 @@ + + + + + + + Your First Listing is Live! + + + +
+
+ +
🎉 Your First Listing is Live!
+
+ +
+

Hi {{ownerName}},

+ +

Congratulations! You're Now a RentAll Host!

+ +

Your first item is officially live and ready to be rented. This is an exciting milestone!

+ +
+
🎊
+
{{itemName}}
+

+ View Your Listing +

+
+ +
+

What happens next?

+
    +
  • Your listing is now searchable by renters
  • +
  • You'll receive email notifications for rental requests
  • +
  • You can approve or decline requests based on your availability
  • +
  • Payments are processed securely through Stripe
  • +
+
+ +

Tips for Success

+ + +
+

🌟 Pro Tip: Hosts who respond within 1 hour get 3x more bookings!

+
+ +

We're excited to have you as part of the RentAll community. If you have any questions, our support team is here to help.

+ +

Happy hosting!
+ The RentAll Team

+
+ + +
+ + diff --git a/backend/templates/emails/lateReturnCS.html b/backend/templates/emails/lateReturnToCS.html similarity index 100% rename from backend/templates/emails/lateReturnCS.html rename to backend/templates/emails/lateReturnToCS.html diff --git a/backend/templates/emails/lostItemCS.html b/backend/templates/emails/lostItemToCS.html similarity index 100% rename from backend/templates/emails/lostItemCS.html rename to backend/templates/emails/lostItemToCS.html diff --git a/backend/templates/emails/passwordChanged.html b/backend/templates/emails/passwordChangedToUser.html similarity index 100% rename from backend/templates/emails/passwordChanged.html rename to backend/templates/emails/passwordChangedToUser.html diff --git a/backend/templates/emails/passwordReset.html b/backend/templates/emails/passwordResetToUser.html similarity index 100% rename from backend/templates/emails/passwordReset.html rename to backend/templates/emails/passwordResetToUser.html diff --git a/backend/templates/emails/payoutReceived.html b/backend/templates/emails/payoutReceivedToOwner.html similarity index 100% rename from backend/templates/emails/payoutReceived.html rename to backend/templates/emails/payoutReceivedToOwner.html diff --git a/backend/templates/emails/rentalApprovalConfirmationToOwner.html b/backend/templates/emails/rentalApprovalConfirmationToOwner.html new file mode 100644 index 0000000..ecca076 --- /dev/null +++ b/backend/templates/emails/rentalApprovalConfirmationToOwner.html @@ -0,0 +1,357 @@ + + + + + + + Rental Request Approved + + + +
+
+ +
Rental Request Approved
+
+ +
+

Hi {{ownerName}},

+ +

You've Approved the Rental Request!

+ +
+
+

Great news! You've successfully approved the rental request for {{itemName}}.

+
+ +

The renter has been notified and {{paymentMessage}}

+ +

Rental Details

+ + + + + + + + + + + + + + + + + + + + + +
Item{{itemName}}
Renter{{renterName}}
Start Date{{startDate}}
End Date{{endDate}}
Delivery Method{{deliveryMethod}}
+ + {{earningsSection}} + + {{stripeSection}} + +

What's Next?

+ + +

+ View Rental Details +

+ +

Thank you for being part of the RentAll community!

+
+ + +
+ + diff --git a/backend/templates/emails/rentalCancellationConfirmation.html b/backend/templates/emails/rentalCancellationConfirmationToUser.html similarity index 100% rename from backend/templates/emails/rentalCancellationConfirmation.html rename to backend/templates/emails/rentalCancellationConfirmationToUser.html diff --git a/backend/templates/emails/rentalCancellationNotification.html b/backend/templates/emails/rentalCancellationNotificationToUser.html similarity index 100% rename from backend/templates/emails/rentalCancellationNotification.html rename to backend/templates/emails/rentalCancellationNotificationToUser.html diff --git a/backend/templates/emails/rentalCompletionCongratsToOwner.html b/backend/templates/emails/rentalCompletionCongratsToOwner.html new file mode 100644 index 0000000..99fffbc --- /dev/null +++ b/backend/templates/emails/rentalCompletionCongratsToOwner.html @@ -0,0 +1,372 @@ + + + + + + + Rental Complete - Congratulations! + + + +
+
+ +
Rental Complete
+
+ +
+

Hi {{ownerName}},

+ +

Congratulations on Completing a Rental!

+ +
+
+

Rental Complete: {{itemName}} has been successfully returned on time.

+

Great job maintaining your item and providing excellent service to the renter!

+
+ +

Rental Summary

+ + + + + + + + + + + + + + + + + +
Item{{itemName}}
Renter{{renterName}}
Rental Period{{startDate}} to {{endDate}}
Returned On{{returnedDate}}
+ + {{earningsSection}} + + {{stripeSection}} + +

Tips for Future Rentals

+ + +

List More Items

+

Have more items sitting idle? Turn them into income! List camping gear, tools, party supplies, sports equipment, or anything else that others might need.

+ +

+ View My Listings +

+ +

Thank you for being an excellent host on RentAll!

+
+ + +
+ + diff --git a/backend/templates/emails/rentalCompletionThankYouToRenter.html b/backend/templates/emails/rentalCompletionThankYouToRenter.html new file mode 100644 index 0000000..5f02d59 --- /dev/null +++ b/backend/templates/emails/rentalCompletionThankYouToRenter.html @@ -0,0 +1,314 @@ + + + + + + + Thank You for Returning On Time! + + + +
+
+ +
Rental Complete
+
+ +
+

Hi {{renterName}},

+ +

Thank You for Returning On Time!

+ +
+
+

Rental Complete: You've successfully returned {{itemName}} on time.

+

On-time returns like yours help build trust in the RentAll community. Thank you!

+
+ +

Rental Summary

+ + + + + + + + + + + + + + + + + +
Item{{itemName}}
Owner{{ownerName}}
Rental Period{{startDate}} to {{endDate}}
Returned On{{returnedDate}}
+ + {{reviewSection}} + +

Keep Renting!

+

Explore thousands of items available for rent in your area. From tools to camping gear, party supplies to photography equipment - find what you need, when you need it.

+ +

+ Browse Available Items +

+ +

Thank you for being a valued member of the RentAll community!

+
+ + +
+ + diff --git a/backend/templates/emails/rentalConfirmation.html b/backend/templates/emails/rentalConfirmationToUser.html similarity index 99% rename from backend/templates/emails/rentalConfirmation.html rename to backend/templates/emails/rentalConfirmationToUser.html index 70ce7b6..5e4c062 100644 --- a/backend/templates/emails/rentalConfirmation.html +++ b/backend/templates/emails/rentalConfirmationToUser.html @@ -251,6 +251,8 @@ + {{paymentSection}} +

What's next?