380 lines
10 KiB
JavaScript
380 lines
10 KiB
JavaScript
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 } = require("../middleware/auth");
|
|
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) {
|
|
console.error("Error in my-rentals route:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
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) {
|
|
console.error("Error in my-listings route:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.post("/", authenticateToken, async (req, res) => {
|
|
try {
|
|
const {
|
|
itemId,
|
|
startDate,
|
|
endDate,
|
|
startTime,
|
|
endTime,
|
|
deliveryMethod,
|
|
deliveryAddress,
|
|
notes,
|
|
} = 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" });
|
|
}
|
|
|
|
const overlappingRental = await Rental.findOne({
|
|
where: {
|
|
itemId,
|
|
status: { [Op.in]: ["confirmed", "active"] },
|
|
[Op.or]: [
|
|
{
|
|
startDate: { [Op.between]: [startDate, endDate] },
|
|
},
|
|
{
|
|
endDate: { [Op.between]: [startDate, endDate] },
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
if (overlappingRental) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: "Item is already booked for these dates" });
|
|
}
|
|
|
|
const rentalDays = Math.ceil(
|
|
(new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24)
|
|
);
|
|
const totalAmount = rentalDays * (item.pricePerDay || 0);
|
|
|
|
const rental = await Rental.create({
|
|
itemId,
|
|
renterId: req.user.id,
|
|
ownerId: item.ownerId,
|
|
startDate,
|
|
endDate,
|
|
startTime,
|
|
endTime,
|
|
totalAmount,
|
|
deliveryMethod,
|
|
deliveryAddress,
|
|
notes,
|
|
});
|
|
|
|
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"],
|
|
},
|
|
],
|
|
});
|
|
|
|
res.status(201).json(rentalWithDetails);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.put("/:id/status", authenticateToken, async (req, res) => {
|
|
try {
|
|
const { status } = 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 && rental.renterId !== req.user.id) {
|
|
return res.status(403).json({ error: "Unauthorized" });
|
|
}
|
|
|
|
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: error.message });
|
|
}
|
|
});
|
|
|
|
// 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: error.message });
|
|
}
|
|
});
|
|
|
|
// 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: error.message });
|
|
}
|
|
});
|
|
|
|
// Mark rental as completed (owner only)
|
|
router.post("/:id/mark-completed", authenticateToken, async (req, res) => {
|
|
try {
|
|
console.log("Mark completed endpoint hit for rental ID:", req.params.id);
|
|
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: error.message });
|
|
}
|
|
});
|
|
|
|
// Legacy review endpoint (for backward compatibility)
|
|
router.post("/:id/review", authenticateToken, async (req, res) => {
|
|
try {
|
|
const { rating, review } = 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 leave reviews" });
|
|
}
|
|
|
|
if (rental.status !== "completed") {
|
|
return res
|
|
.status(400)
|
|
.json({ error: "Can only review completed rentals" });
|
|
}
|
|
|
|
await rental.update({ rating, review });
|
|
|
|
res.json(rental);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|