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:
jackiettran
2025-10-28 22:23:41 -04:00
parent 502d84a741
commit d1cb857aa7
25 changed files with 2171 additions and 53 deletions

View File

@@ -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);

View File

@@ -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) => {

View File

@@ -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":