const express = require("express"); const { Op } = require("sequelize"); const { Rental, Item, User } = require("../models"); // Import from models/index.js to get models with associations const { authenticateToken, requireVerifiedEmail } = require("../middleware/auth"); const FeeCalculator = require("../utils/feeCalculator"); const RefundService = require("../services/refundService"); const LateReturnService = require("../services/lateReturnService"); const DamageAssessmentService = require("../services/damageAssessmentService"); const emailService = require("../services/emailService"); const logger = require("../utils/logger"); const router = express.Router(); // Helper function to check and update review visibility const checkAndUpdateReviewVisibility = async (rental) => { const now = new Date(); const tenMinutesInMs = 10 * 60 * 1000; // 10 minutes let needsUpdate = false; let updates = {}; // Check if both reviews are submitted if (rental.itemReviewSubmittedAt && rental.renterReviewSubmittedAt) { if (!rental.itemReviewVisible || !rental.renterReviewVisible) { updates.itemReviewVisible = true; updates.renterReviewVisible = true; needsUpdate = true; } } else { // Check item review visibility (10-minute rule) if (rental.itemReviewSubmittedAt && !rental.itemReviewVisible) { const timeSinceSubmission = now - new Date(rental.itemReviewSubmittedAt); if (timeSinceSubmission >= tenMinutesInMs) { updates.itemReviewVisible = true; needsUpdate = true; } } // Check renter review visibility (10-minute rule) if (rental.renterReviewSubmittedAt && !rental.renterReviewVisible) { const timeSinceSubmission = now - new Date(rental.renterReviewSubmittedAt); if (timeSinceSubmission >= tenMinutesInMs) { updates.renterReviewVisible = true; needsUpdate = true; } } } if (needsUpdate) { await rental.update(updates); } return rental; }; router.get("/my-rentals", authenticateToken, async (req, res) => { try { const rentals = await Rental.findAll({ where: { renterId: req.user.id }, // Remove explicit attributes to let Sequelize handle missing columns gracefully include: [ { model: Item, as: "item" }, { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, ], order: [["createdAt", "DESC"]], }); res.json(rentals); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error in my-rentals route", { error: error.message, stack: error.stack, userId: req.user.id, }); res.status(500).json({ error: "Failed to fetch rentals" }); } }); router.get("/my-listings", authenticateToken, async (req, res) => { try { const rentals = await Rental.findAll({ where: { ownerId: req.user.id }, // Remove explicit attributes to let Sequelize handle missing columns gracefully include: [ { model: Item, as: "item" }, { model: User, as: "renter", attributes: ["id", "username", "firstName", "lastName"], }, ], order: [["createdAt", "DESC"]], }); res.json(rentals); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error in my-listings route", { error: error.message, stack: error.stack, userId: req.user.id, }); res.status(500).json({ error: "Failed to fetch listings" }); } }); // Get rental by ID router.get("/:id", authenticateToken, async (req, res) => { try { const rental = await Rental.findByPk(req.params.id, { include: [ { model: Item, as: "item" }, { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, { model: User, as: "renter", attributes: ["id", "username", "firstName", "lastName"], }, ], }); if (!rental) { return res.status(404).json({ error: "Rental not found" }); } // Check if user is authorized to view this rental if (rental.ownerId !== req.user.id && rental.renterId !== req.user.id) { return res .status(403) .json({ error: "Unauthorized to view this rental" }); } res.json(rental); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error fetching rental", { error: error.message, stack: error.stack, rentalId: req.params.id, userId: req.user.id, }); res.status(500).json({ error: "Failed to fetch rental" }); } }); router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => { try { const { itemId, startDateTime, endDateTime, deliveryMethod, deliveryAddress, notes, stripePaymentMethodId, } = req.body; const item = await Item.findByPk(itemId); if (!item) { return res.status(404).json({ error: "Item not found" }); } if (!item.availability) { return res.status(400).json({ error: "Item is not available" }); } let rentalStartDateTime, rentalEndDateTime, totalAmount; // New UTC datetime format rentalStartDateTime = new Date(startDateTime); rentalEndDateTime = new Date(endDateTime); // Calculate rental duration const diffMs = rentalEndDateTime.getTime() - rentalStartDateTime.getTime(); const diffHours = Math.ceil(diffMs / (1000 * 60 * 60)); const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24)); // Calculate base amount based on duration if (item.pricePerHour && diffHours <= 24) { totalAmount = diffHours * Number(item.pricePerHour); } else if (item.pricePerDay) { totalAmount = diffDays * Number(item.pricePerDay); } else { totalAmount = 0; } // Check for overlapping rentals using datetime ranges const overlappingRental = await Rental.findOne({ where: { itemId, status: { [Op.in]: ["confirmed", "active"] }, [Op.or]: [ { [Op.and]: [ { startDateTime: { [Op.not]: null } }, { endDateTime: { [Op.not]: null } }, { [Op.or]: [ { startDateTime: { [Op.between]: [rentalStartDateTime, rentalEndDateTime], }, }, { endDateTime: { [Op.between]: [rentalStartDateTime, rentalEndDateTime], }, }, { [Op.and]: [ { startDateTime: { [Op.lte]: rentalStartDateTime } }, { endDateTime: { [Op.gte]: rentalEndDateTime } }, ], }, ], }, ], }, ], }, }); if (overlappingRental) { return res .status(400) .json({ error: "Item is already booked for these dates" }); } // Calculate fees using FeeCalculator const fees = FeeCalculator.calculateRentalFees(totalAmount); // Validate that payment method was provided for paid rentals if (totalAmount > 0 && !stripePaymentMethodId) { return res .status(400) .json({ error: "Payment method is required for paid rentals" }); } const rentalData = { itemId, renterId: req.user.id, ownerId: item.ownerId, startDateTime: rentalStartDateTime, endDateTime: rentalEndDateTime, totalAmount: fees.totalChargedAmount, platformFee: fees.platformFee, payoutAmount: fees.payoutAmount, paymentStatus: totalAmount > 0 ? "pending" : "not_required", status: "pending", deliveryMethod, deliveryAddress, notes, }; // Only add stripePaymentMethodId if it's provided (for paid rentals) if (stripePaymentMethodId) { rentalData.stripePaymentMethodId = stripePaymentMethodId; } const rental = await Rental.create(rentalData); const rentalWithDetails = await Rental.findByPk(rental.id, { include: [ { model: Item, as: "item" }, { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, { model: User, as: "renter", attributes: ["id", "username", "firstName", "lastName"], }, ], }); // Send rental request notification to owner try { await emailService.sendRentalRequestEmail(rentalWithDetails); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Rental request notification sent to owner", { rentalId: rental.id, ownerId: rentalWithDetails.ownerId, }); } catch (emailError) { // Log error but don't fail the request const reqLogger = logger.withRequestId(req.id); reqLogger.error("Failed to send rental request email", { error: emailError.message, rentalId: rental.id, ownerId: rentalWithDetails.ownerId, }); } res.status(201).json(rentalWithDetails); } catch (error) { res.status(500).json({ error: "Failed to create rental" }); } }); router.put("/:id/status", authenticateToken, async (req, res) => { try { const { status } = req.body; const rental = await Rental.findByPk(req.params.id, { include: [ { model: Item, as: "item" }, { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, { model: User, as: "renter", attributes: [ "id", "username", "firstName", "lastName", "stripeCustomerId", ], }, ], }); if (!rental) { return res.status(404).json({ error: "Rental not found" }); } if (rental.ownerId !== req.user.id && rental.renterId !== req.user.id) { return res .status(403) .json({ error: "Unauthorized to update this rental" }); } // If owner is approving a pending rental, handle payment for paid rentals if ( status === "confirmed" && rental.status === "pending" && rental.ownerId === req.user.id ) { // Skip payment processing for free rentals if (rental.totalAmount > 0) { if (!rental.stripePaymentMethodId) { return res .status(400) .json({ error: "No payment method found for this rental" }); } try { // Import StripeService to process the payment const StripeService = require("../services/stripeService"); // Check if renter has a stripe customer ID if (!rental.renter.stripeCustomerId) { return res.status(400).json({ error: "Renter does not have a Stripe customer account", }); } // Create payment intent and charge the stored payment method const paymentResult = await StripeService.chargePaymentMethod( rental.stripePaymentMethodId, rental.totalAmount, rental.renter.stripeCustomerId, { rentalId: rental.id, itemName: rental.item.name, renterId: rental.renterId, ownerId: rental.ownerId, } ); // Update rental with payment completion await rental.update({ status: "confirmed", paymentStatus: "paid", stripePaymentIntentId: paymentResult.paymentIntentId, }); const updatedRental = await Rental.findByPk(rental.id, { include: [ { model: Item, as: "item" }, { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, { model: User, as: "renter", attributes: ["id", "username", "firstName", "lastName"], }, ], }); // Send confirmation emails await emailService.sendRentalConfirmationEmails(updatedRental); res.json(updatedRental); return; } catch (paymentError) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Payment failed during approval", { error: paymentError.message, stack: paymentError.stack, rentalId: req.params.id, userId: req.user.id, }); // Keep rental as pending, but inform of payment failure return res.status(400).json({ error: "Payment failed during approval", details: paymentError.message, }); } } else { // For free rentals, just update status directly await rental.update({ status: "confirmed", }); const updatedRental = await Rental.findByPk(rental.id, { include: [ { model: Item, as: "item" }, { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, { model: User, as: "renter", attributes: ["id", "username", "firstName", "lastName"], }, ], }); // Send confirmation emails await emailService.sendRentalConfirmationEmails(updatedRental); res.json(updatedRental); return; } } await rental.update({ status }); const updatedRental = await Rental.findByPk(rental.id, { include: [ { model: Item, as: "item" }, { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, { model: User, as: "renter", attributes: ["id", "username", "firstName", "lastName"], }, ], }); res.json(updatedRental); } catch (error) { res.status(500).json({ error: "Failed to update rental status" }); } }); // Owner reviews renter router.post("/:id/review-renter", authenticateToken, async (req, res) => { try { const { rating, review, privateMessage } = req.body; const rental = await Rental.findByPk(req.params.id); if (!rental) { return res.status(404).json({ error: "Rental not found" }); } if (rental.ownerId !== req.user.id) { return res.status(403).json({ error: "Only owners can review renters" }); } if (rental.status !== "completed") { return res .status(400) .json({ error: "Can only review completed rentals" }); } if (rental.renterReviewSubmittedAt) { return res.status(400).json({ error: "Renter review already submitted" }); } // Submit the review and private message await rental.update({ renterRating: rating, renterReview: review, renterReviewSubmittedAt: new Date(), renterPrivateMessage: privateMessage, }); // Check and update visibility const updatedRental = await checkAndUpdateReviewVisibility(rental); res.json({ success: true, reviewVisible: updatedRental.renterReviewVisible, }); } catch (error) { res.status(500).json({ error: "Failed to submit review" }); } }); // Renter reviews item router.post("/:id/review-item", authenticateToken, async (req, res) => { try { const { rating, review, privateMessage } = req.body; const rental = await Rental.findByPk(req.params.id); if (!rental) { return res.status(404).json({ error: "Rental not found" }); } if (rental.renterId !== req.user.id) { return res.status(403).json({ error: "Only renters can review items" }); } if (rental.status !== "completed") { return res .status(400) .json({ error: "Can only review completed rentals" }); } if (rental.itemReviewSubmittedAt) { return res.status(400).json({ error: "Item review already submitted" }); } // Submit the review and private message await rental.update({ itemRating: rating, itemReview: review, itemReviewSubmittedAt: new Date(), itemPrivateMessage: privateMessage, }); // Check and update visibility const updatedRental = await checkAndUpdateReviewVisibility(rental); res.json({ success: true, reviewVisible: updatedRental.itemReviewVisible, }); } catch (error) { res.status(500).json({ error: "Failed to submit review" }); } }); // Mark rental as completed (owner only) router.post("/:id/mark-completed", authenticateToken, async (req, res) => { try { const rental = await Rental.findByPk(req.params.id); if (!rental) { return res.status(404).json({ error: "Rental not found" }); } if (rental.ownerId !== req.user.id) { return res .status(403) .json({ error: "Only owners can mark rentals as completed" }); } if (!["active", "confirmed"].includes(rental.status)) { return res.status(400).json({ error: "Can only mark active or confirmed rentals as completed", }); } await rental.update({ status: "completed" }); const updatedRental = await Rental.findByPk(rental.id, { include: [ { model: Item, as: "item" }, { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, { model: User, as: "renter", attributes: ["id", "username", "firstName", "lastName"], }, ], }); res.json(updatedRental); } catch (error) { res.status(500).json({ error: "Failed to update rental" }); } }); // Calculate fees for rental pricing display router.post("/calculate-fees", authenticateToken, async (req, res) => { try { const { totalAmount } = req.body; if (!totalAmount || totalAmount <= 0) { return res.status(400).json({ error: "Valid base amount is required" }); } const fees = FeeCalculator.calculateRentalFees(totalAmount); const displayFees = FeeCalculator.formatFeesForDisplay(fees); res.json({ fees, display: displayFees, }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error calculating fees", { error: error.message, stack: error.stack, userId: req.user.id, startDate: req.query.startDate, endDate: req.query.endDate, itemId: req.query.itemId, }); res.status(500).json({ error: "Failed to calculate fees" }); } }); // Get earnings status for owner's rentals router.get("/earnings/status", authenticateToken, async (req, res) => { try { const ownerRentals = await Rental.findAll({ where: { ownerId: req.user.id, status: "completed", }, attributes: [ "id", "totalAmount", "platformFee", "payoutAmount", "payoutStatus", "payoutProcessedAt", "stripeTransferId", ], include: [{ model: Item, as: "item", attributes: ["name"] }], order: [["createdAt", "DESC"]], }); res.json(ownerRentals); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error getting earnings status", { error: error.message, stack: error.stack, userId: req.user.id, }); res.status(500).json({ error: error.message }); } }); // Get refund preview (what would happen if cancelled now) router.get("/:id/refund-preview", authenticateToken, async (req, res) => { try { const preview = await RefundService.getRefundPreview( req.params.id, req.user.id ); res.json(preview); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error getting refund preview", { error: error.message, stack: error.stack, rentalId: req.params.id, userId: req.user.id, }); res.status(400).json({ error: error.message }); } }); // Get late fee preview router.get("/:id/late-fee-preview", authenticateToken, async (req, res) => { try { const { actualReturnDateTime } = req.query; if (!actualReturnDateTime) { return res.status(400).json({ error: "actualReturnDateTime is required" }); } const rental = await Rental.findByPk(req.params.id, { include: [{ model: Item, as: "item" }], }); if (!rental) { return res.status(404).json({ error: "Rental not found" }); } // Check authorization if (rental.ownerId !== req.user.id && rental.renterId !== req.user.id) { return res.status(403).json({ error: "Unauthorized" }); } const lateCalculation = LateReturnService.calculateLateFee( rental, actualReturnDateTime ); res.json(lateCalculation); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error getting late fee preview", { error: error.message, stack: error.stack, rentalId: req.params.id, userId: req.user.id, }); res.status(400).json({ error: error.message }); } }); // Cancel rental with refund processing router.post("/:id/cancel", authenticateToken, async (req, res) => { try { const { reason } = req.body; const result = await RefundService.processCancellation( req.params.id, req.user.id, reason ); // Return the updated rental with refund information const updatedRental = await Rental.findByPk(result.rental.id, { include: [ { model: Item, as: "item" }, { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, { model: User, as: "renter", attributes: ["id", "username", "firstName", "lastName"], }, ], }); res.json({ rental: updatedRental, refund: result.refund, }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error cancelling rental", { error: error.message, stack: error.stack, rentalId: req.params.id, userId: req.user.id, }); res.status(400).json({ error: error.message }); } }); // Mark item return status (owner only) router.post("/:id/mark-return", authenticateToken, async (req, res) => { try { const { status, actualReturnDateTime, notes, statusOptions } = req.body; const rentalId = req.params.id; const userId = req.user.id; const rental = await Rental.findByPk(rentalId, { include: [{ model: Item, as: "item" }], }); if (!rental) { return res.status(404).json({ error: "Rental not found" }); } if (rental.ownerId !== userId) { return res .status(403) .json({ error: "Only the item owner can mark return status" }); } if (!["confirmed", "active"].includes(rental.status)) { return res.status(400).json({ error: "Can only mark return status for confirmed or active rentals", }); } let updatedRental; let additionalInfo = {}; switch (status) { case "returned": // Item returned on time updatedRental = await rental.update({ status: "completed", actualReturnDateTime: actualReturnDateTime || rental.endDateTime, notes: notes || null, }); break; case "damaged": // Item returned damaged const damageUpdates = { status: "damaged", actualReturnDateTime: actualReturnDateTime || rental.endDateTime, notes: notes || null, }; // Check if ALSO returned late if (statusOptions?.returned_late && actualReturnDateTime) { const lateReturnDamaged = await LateReturnService.processLateReturn( rentalId, actualReturnDateTime, notes ); damageUpdates.status = "returned_late_and_damaged"; damageUpdates.lateFees = lateReturnDamaged.lateCalculation.lateFee; damageUpdates.actualReturnDateTime = lateReturnDamaged.rental.actualReturnDateTime; additionalInfo.lateCalculation = lateReturnDamaged.lateCalculation; } updatedRental = await rental.update(damageUpdates); break; case "returned_late": // Item returned late - calculate late fees if (!actualReturnDateTime) { return res.status(400).json({ error: "Actual return date/time is required for late returns", }); } const lateReturn = await LateReturnService.processLateReturn( rentalId, actualReturnDateTime, notes ); updatedRental = lateReturn.rental; additionalInfo.lateCalculation = lateReturn.lateCalculation; break; case "lost": // Item reported as lost updatedRental = await rental.update({ status: "lost", itemLostReportedAt: new Date(), notes: notes || null, }); // Send notification to customer service await emailService.sendLostItemToCustomerService(updatedRental); break; default: return res.status(400).json({ error: "Invalid status. Use 'returned', 'returned_late', 'damaged', or 'lost'", }); } const reqLogger = logger.withRequestId(req.id); reqLogger.info("Return status marked", { rentalId, status, ownerId: userId, lateFee: updatedRental.lateFees || 0, }); res.json({ success: true, rental: updatedRental, ...additionalInfo, }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error marking return status", { error: error.message, rentalId: req.params.id, userId: req.user.id, }); res.status(400).json({ error: error.message }); } }); // Report item as damaged (owner only) router.post("/:id/report-damage", authenticateToken, async (req, res) => { try { const rentalId = req.params.id; const userId = req.user.id; const damageInfo = req.body; const result = await DamageAssessmentService.processDamageAssessment( rentalId, damageInfo, userId ); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Damage reported", { rentalId, ownerId: userId, damageFee: result.damageAssessment.feeCalculation.amount, lateFee: result.lateCalculation?.lateFee || 0, }); res.json({ success: true, ...result, }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error reporting damage", { error: error.message, rentalId: req.params.id, userId: req.user.id, }); res.status(400).json({ error: error.message }); } }); module.exports = router;