const express = require("express"); const { Op } = require("sequelize"); const { Item, User, Rental } = require("../models"); // Import from models/index.js to get models with associations const { authenticateToken, requireVerifiedEmail } = require("../middleware/auth"); const logger = require("../utils/logger"); const router = express.Router(); router.get("/", async (req, res) => { try { const { minPrice, maxPrice, city, zipCode, search, page = 1, limit = 20, } = req.query; const where = {}; if (minPrice || maxPrice) { where.pricePerDay = {}; if (minPrice) where.pricePerDay[Op.gte] = minPrice; if (maxPrice) where.pricePerDay[Op.lte] = maxPrice; } if (city) where.city = { [Op.iLike]: `%${city}%` }; if (zipCode) where.zipCode = { [Op.iLike]: `%${zipCode}%` }; if (search) { where[Op.or] = [ { name: { [Op.iLike]: `%${search}%` } }, { description: { [Op.iLike]: `%${search}%` } }, ]; } const offset = (page - 1) * limit; const { count, rows } = await Item.findAndCountAll({ where, include: [ { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, ], limit: parseInt(limit), offset: parseInt(offset), order: [["createdAt", "DESC"]], }); // Round coordinates to 2 decimal places for map display while keeping precise values in database const itemsWithRoundedCoords = rows.map(item => { const itemData = item.toJSON(); if (itemData.latitude !== null && itemData.latitude !== undefined) { itemData.latitude = Math.round(parseFloat(itemData.latitude) * 100) / 100; } if (itemData.longitude !== null && itemData.longitude !== undefined) { itemData.longitude = Math.round(parseFloat(itemData.longitude) * 100) / 100; } return itemData; }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Items search completed", { filters: { minPrice, maxPrice, city, zipCode, search }, resultsCount: count, page: parseInt(page), limit: parseInt(limit) }); res.json({ items: itemsWithRoundedCoords, totalPages: Math.ceil(count / limit), currentPage: parseInt(page), totalItems: count, }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Items search failed", { error: error.message, stack: error.stack, query: req.query }); res.status(500).json({ error: error.message }); } }); router.get("/recommendations", authenticateToken, async (req, res) => { try { const userRentals = await Rental.findAll({ where: { renterId: req.user.id }, include: [{ model: Item, as: "item" }], }); // For now, just return random available items as recommendations const recommendations = await Item.findAll({ where: { availability: true, }, limit: 10, order: [["createdAt", "DESC"]], }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Recommendations fetched", { userId: req.user.id, recommendationsCount: recommendations.length }); res.json(recommendations); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Recommendations fetch failed", { error: error.message, stack: error.stack, userId: req.user.id }); res.status(500).json({ error: error.message }); } }); // Public endpoint to get reviews for a specific item (must come before /:id route) router.get('/:id/reviews', async (req, res) => { try { const { Rental, User } = require('../models'); const reviews = await Rental.findAll({ where: { itemId: req.params.id, status: 'completed', itemRating: { [Op.not]: null }, itemReview: { [Op.not]: null }, itemReviewVisible: true }, include: [ { model: User, as: 'renter', attributes: ['id', 'firstName', 'lastName'] } ], order: [['createdAt', 'DESC']] }); const averageRating = reviews.length > 0 ? reviews.reduce((sum, review) => sum + review.itemRating, 0) / reviews.length : 0; const reqLogger = logger.withRequestId(req.id); reqLogger.info("Item reviews fetched", { itemId: req.params.id, reviewsCount: reviews.length, averageRating }); res.json({ reviews, averageRating, totalReviews: reviews.length }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Item reviews fetch failed", { error: error.message, stack: error.stack, itemId: req.params.id }); res.status(500).json({ error: error.message }); } }); router.get("/:id", async (req, res) => { try { const item = await Item.findByPk(req.params.id, { include: [ { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, ], }); if (!item) { return res.status(404).json({ error: "Item not found" }); } // Round coordinates to 2 decimal places for map display while keeping precise values in database const itemResponse = item.toJSON(); if (itemResponse.latitude !== null && itemResponse.latitude !== undefined) { itemResponse.latitude = Math.round(parseFloat(itemResponse.latitude) * 100) / 100; } if (itemResponse.longitude !== null && itemResponse.longitude !== undefined) { itemResponse.longitude = Math.round(parseFloat(itemResponse.longitude) * 100) / 100; } const reqLogger = logger.withRequestId(req.id); reqLogger.info("Item fetched", { itemId: req.params.id, ownerId: item.ownerId }); res.json(itemResponse); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Item fetch failed", { error: error.message, stack: error.stack, itemId: req.params.id }); res.status(500).json({ error: error.message }); } }); router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => { try { const item = await Item.create({ ...req.body, ownerId: req.user.id, }); const itemWithOwner = await Item.findByPk(item.id, { include: [ { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, ], }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Item created", { itemId: item.id, ownerId: req.user.id, itemName: req.body.name }); res.status(201).json(itemWithOwner); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Item creation failed", { error: error.message, stack: error.stack, ownerId: req.user.id, itemData: logger.sanitize(req.body) }); res.status(500).json({ error: error.message }); } }); router.put("/:id", authenticateToken, async (req, res) => { try { const item = await Item.findByPk(req.params.id); if (!item) { return res.status(404).json({ error: "Item not found" }); } if (item.ownerId !== req.user.id) { return res.status(403).json({ error: "Unauthorized" }); } await item.update(req.body); const updatedItem = await Item.findByPk(item.id, { include: [ { model: User, as: "owner", attributes: ["id", "username", "firstName", "lastName"], }, ], }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Item updated", { itemId: req.params.id, ownerId: req.user.id }); res.json(updatedItem); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Item update failed", { error: error.message, stack: error.stack, itemId: req.params.id, ownerId: req.user.id }); res.status(500).json({ error: error.message }); } }); router.delete("/:id", authenticateToken, async (req, res) => { try { const item = await Item.findByPk(req.params.id); if (!item) { return res.status(404).json({ error: "Item not found" }); } if (item.ownerId !== req.user.id) { return res.status(403).json({ error: "Unauthorized" }); } await item.destroy(); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Item deleted", { itemId: req.params.id, ownerId: req.user.id }); res.status(204).send(); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Item deletion failed", { error: error.message, stack: error.stack, itemId: req.params.id, ownerId: req.user.id }); res.status(500).json({ error: error.message }); } }); module.exports = router;