email plus return item statuses
This commit is contained in:
165
backend/routes/conditionChecks.js
Normal file
165
backend/routes/conditionChecks.js
Normal file
@@ -0,0 +1,165 @@
|
||||
const express = require("express");
|
||||
const multer = require("multer");
|
||||
const { authenticateToken } = require("../middleware/auth");
|
||||
const ConditionCheckService = require("../services/conditionCheckService");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Configure multer for photo uploads
|
||||
const upload = multer({
|
||||
dest: "uploads/condition-checks/",
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB limit
|
||||
files: 20, // Maximum 20 files
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
// Accept only image files
|
||||
if (file.mimetype.startsWith("image/")) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error("Only image files are allowed"), false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Submit a condition check
|
||||
router.post(
|
||||
"/:rentalId",
|
||||
authenticateToken,
|
||||
upload.array("photos"),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { rentalId } = req.params;
|
||||
const { checkType, notes } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
// Get uploaded file paths
|
||||
const photos = req.files ? req.files.map((file) => file.path) : [];
|
||||
|
||||
// Extract metadata from request
|
||||
const metadata = {
|
||||
userAgent: req.get("User-Agent"),
|
||||
ipAddress: req.ip,
|
||||
deviceType: req.get("X-Device-Type") || "web",
|
||||
};
|
||||
|
||||
const conditionCheck = await ConditionCheckService.submitConditionCheck(
|
||||
rentalId,
|
||||
checkType,
|
||||
userId,
|
||||
photos,
|
||||
notes,
|
||||
metadata
|
||||
);
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Condition check submitted", {
|
||||
rentalId,
|
||||
checkType,
|
||||
userId,
|
||||
photoCount: photos.length,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
conditionCheck,
|
||||
});
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Error submitting condition check", {
|
||||
error: error.message,
|
||||
rentalId: req.params.rentalId,
|
||||
userId: req.user?.id,
|
||||
});
|
||||
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Get condition checks for a rental
|
||||
router.get("/:rentalId", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { rentalId } = req.params;
|
||||
|
||||
const conditionChecks = await ConditionCheckService.getConditionChecks(
|
||||
rentalId
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
conditionChecks,
|
||||
});
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Error fetching condition checks", {
|
||||
error: error.message,
|
||||
rentalId: req.params.rentalId,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to fetch condition checks",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get condition check timeline for a rental
|
||||
router.get("/:rentalId/timeline", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { rentalId } = req.params;
|
||||
|
||||
const timeline = await ConditionCheckService.getConditionCheckTimeline(
|
||||
rentalId
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
timeline,
|
||||
});
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Error fetching condition check timeline", {
|
||||
error: error.message,
|
||||
rentalId: req.params.rentalId,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get available condition checks for current user
|
||||
router.get("/", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
const availableChecks = await ConditionCheckService.getAvailableChecks(
|
||||
userId
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
availableChecks,
|
||||
});
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Error fetching available checks", {
|
||||
error: error.message,
|
||||
userId: req.user?.id,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to fetch available checks",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -4,6 +4,9 @@ const { Rental, Item, User } = require("../models"); // Import from models/index
|
||||
const { authenticateToken } = 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();
|
||||
|
||||
@@ -72,7 +75,7 @@ router.get("/my-rentals", authenticateToken, async (req, res) => {
|
||||
reqLogger.error("Error in my-rentals route", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
userId: req.user.id
|
||||
userId: req.user.id,
|
||||
});
|
||||
res.status(500).json({ error: "Failed to fetch rentals" });
|
||||
}
|
||||
@@ -100,7 +103,7 @@ router.get("/my-listings", authenticateToken, async (req, res) => {
|
||||
reqLogger.error("Error in my-listings route", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
userId: req.user.id
|
||||
userId: req.user.id,
|
||||
});
|
||||
res.status(500).json({ error: "Failed to fetch listings" });
|
||||
}
|
||||
@@ -131,7 +134,9 @@ router.get("/:id", authenticateToken, async (req, res) => {
|
||||
|
||||
// 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" });
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Unauthorized to view this rental" });
|
||||
}
|
||||
|
||||
res.json(rental);
|
||||
@@ -141,7 +146,7 @@ router.get("/:id", authenticateToken, async (req, res) => {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
rentalId: req.params.id,
|
||||
userId: req.user.id
|
||||
userId: req.user.id,
|
||||
});
|
||||
res.status(500).json({ error: "Failed to fetch rental" });
|
||||
}
|
||||
@@ -235,7 +240,9 @@ router.post("/", authenticateToken, async (req, res) => {
|
||||
|
||||
// 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" });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Payment method is required for paid rentals" });
|
||||
}
|
||||
|
||||
const rentalData = {
|
||||
@@ -313,7 +320,9 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
}
|
||||
|
||||
if (rental.ownerId !== req.user.id && rental.renterId !== req.user.id) {
|
||||
return res.status(403).json({ error: "Unauthorized to update this rental" });
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Unauthorized to update this rental" });
|
||||
}
|
||||
|
||||
// If owner is approving a pending rental, handle payment for paid rentals
|
||||
@@ -330,73 +339,76 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
.json({ error: "No payment method found for this rental" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Import StripeService to process the payment
|
||||
const StripeService = require("../services/stripeService");
|
||||
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,
|
||||
// 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",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// 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" },
|
||||
// Create payment intent and charge the stored payment method
|
||||
const paymentResult = await StripeService.chargePaymentMethod(
|
||||
rental.stripePaymentMethodId,
|
||||
rental.totalAmount,
|
||||
rental.renter.stripeCustomerId,
|
||||
{
|
||||
model: User,
|
||||
as: "owner",
|
||||
attributes: ["id", "username", "firstName", "lastName"],
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: "renter",
|
||||
attributes: ["id", "username", "firstName", "lastName"],
|
||||
},
|
||||
],
|
||||
});
|
||||
rentalId: rental.id,
|
||||
itemName: rental.item.name,
|
||||
renterId: rental.renterId,
|
||||
ownerId: rental.ownerId,
|
||||
}
|
||||
);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
// 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"
|
||||
status: "confirmed",
|
||||
});
|
||||
|
||||
const updatedRental = await Rental.findByPk(rental.id, {
|
||||
@@ -415,6 +427,9 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
],
|
||||
});
|
||||
|
||||
// Send confirmation emails
|
||||
await emailService.sendRentalConfirmationEmails(updatedRental);
|
||||
|
||||
res.json(updatedRental);
|
||||
return;
|
||||
}
|
||||
@@ -601,7 +616,7 @@ router.post("/calculate-fees", authenticateToken, async (req, res) => {
|
||||
userId: req.user.id,
|
||||
startDate: req.query.startDate,
|
||||
endDate: req.query.endDate,
|
||||
itemId: req.query.itemId
|
||||
itemId: req.query.itemId,
|
||||
});
|
||||
res.status(500).json({ error: "Failed to calculate fees" });
|
||||
}
|
||||
@@ -634,7 +649,7 @@ router.get("/earnings/status", authenticateToken, async (req, res) => {
|
||||
reqLogger.error("Error getting earnings status", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
userId: req.user.id
|
||||
userId: req.user.id,
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
@@ -654,7 +669,47 @@ router.get("/:id/refund-preview", authenticateToken, async (req, res) => {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
rentalId: req.params.id,
|
||||
userId: req.user.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 });
|
||||
}
|
||||
@@ -698,10 +753,174 @@ router.post("/:id/cancel", authenticateToken, async (req, res) => {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
rentalId: req.params.id,
|
||||
userId: req.user.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;
|
||||
|
||||
Reference in New Issue
Block a user