diff --git a/backend/jobs/rentalStatusJob.js b/backend/jobs/rentalStatusJob.js deleted file mode 100644 index 8281767..0000000 --- a/backend/jobs/rentalStatusJob.js +++ /dev/null @@ -1,101 +0,0 @@ -const cron = require("node-cron"); -const { Rental } = require("../models"); -const { Op } = require("sequelize"); -const logger = require("../utils/logger"); - -const statusUpdateSchedule = "*/15 * * * *"; // Run every 15 minutes - -class RentalStatusJob { - static startScheduledStatusUpdates() { - console.log("Starting automated rental status updates..."); - - const statusJob = cron.schedule( - statusUpdateSchedule, - async () => { - try { - await this.activateStartedRentals(); - } catch (error) { - logger.error("Error in scheduled rental status update", { - error: error.message, - stack: error.stack - }); - } - }, - { - scheduled: false, - timezone: "America/New_York", - } - ); - - // Start the job - statusJob.start(); - - console.log("Rental status job scheduled:"); - console.log("- Status updates every 15 minutes: " + statusUpdateSchedule); - - return { - statusJob, - - stop() { - statusJob.stop(); - console.log("Rental status job stopped"); - }, - - getStatus() { - return { - statusJobRunning: statusJob.getStatus() === "scheduled", - }; - }, - }; - } - - static async activateStartedRentals() { - try { - const now = new Date(); - - // Find all confirmed rentals where start time has arrived - const rentalsToActivate = await Rental.findAll({ - where: { - status: "confirmed", - startDateTime: { - [Op.lte]: now, - }, - }, - }); - - if (rentalsToActivate.length === 0) { - return { activated: 0 }; - } - - // Update all matching rentals to active status - const rentalIds = rentalsToActivate.map((r) => r.id); - const [updateCount] = await Rental.update( - { status: "active" }, - { - where: { - id: { - [Op.in]: rentalIds, - }, - }, - } - ); - - logger.info("Activated started rentals", { - count: updateCount, - rentalIds: rentalIds, - }); - - console.log(`Activated ${updateCount} rentals that have started`); - - return { activated: updateCount, rentalIds }; - } catch (error) { - logger.error("Error activating started rentals", { - error: error.message, - stack: error.stack, - }); - throw error; - } - } -} - -module.exports = RentalStatusJob; diff --git a/backend/routes/rentals.js b/backend/routes/rentals.js index c26ab1b..acb7b49 100644 --- a/backend/routes/rentals.js +++ b/backend/routes/rentals.js @@ -14,8 +14,24 @@ const emailServices = require("../services/email"); const logger = require("../utils/logger"); const { validateS3Keys } = require("../utils/s3KeyValidator"); const { IMAGE_LIMITS } = require("../config/imageLimits"); +const { isActive, getEffectiveStatus } = require("../utils/rentalStatus"); const router = express.Router(); +// Helper to add displayStatus to rental response +const addDisplayStatus = (rental) => { + if (!rental) return rental; + const rentalJson = rental.toJSON ? rental.toJSON() : rental; + return { + ...rentalJson, + displayStatus: getEffectiveStatus(rentalJson), + }; +}; + +// Helper to add displayStatus to array of rentals +const addDisplayStatusToArray = (rentals) => { + return rentals.map(addDisplayStatus); +}; + // Helper function to check and update review visibility const checkAndUpdateReviewVisibility = async (rental) => { const now = new Date(); @@ -75,7 +91,7 @@ router.get("/renting", authenticateToken, async (req, res) => { order: [["createdAt", "DESC"]], }); - res.json(rentals); + res.json(addDisplayStatusToArray(rentals)); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error in renting route", { @@ -103,7 +119,7 @@ router.get("/owning", authenticateToken, async (req, res) => { order: [["createdAt", "DESC"]], }); - res.json(rentals); + res.json(addDisplayStatusToArray(rentals)); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error in owning route", { @@ -167,7 +183,7 @@ router.get("/:id", authenticateToken, async (req, res) => { .json({ error: "Unauthorized to view this rental" }); } - res.json(rental); + res.json(addDisplayStatus(rental)); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error fetching rental", { @@ -235,10 +251,11 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => { ); // Check for overlapping rentals using datetime ranges + // Note: "active" rentals are stored as "confirmed" with startDateTime in the past const overlappingRental = await Rental.findOne({ where: { itemId, - status: { [Op.in]: ["confirmed", "active"] }, + status: "confirmed", [Op.or]: [ { [Op.and]: [ @@ -875,7 +892,7 @@ router.post("/:id/mark-completed", authenticateToken, async (req, res) => { .json({ error: "Only owners can mark rentals as completed" }); } - if (rental.status !== "active") { + if (!isActive(rental)) { return res.status(400).json({ error: "Can only mark active rentals as completed", }); @@ -1197,7 +1214,7 @@ router.post("/:id/mark-return", authenticateToken, async (req, res, next) => { .json({ error: "Only the item owner can mark return status" }); } - if (rental.status !== "active") { + if (!isActive(rental)) { return res.status(400).json({ error: "Can only mark return status for active rentals", }); diff --git a/backend/server.js b/backend/server.js index c0bcba9..872d9c5 100644 --- a/backend/server.js +++ b/backend/server.js @@ -32,7 +32,6 @@ const uploadRoutes = require("./routes/upload"); const healthRoutes = require("./routes/health"); const PayoutProcessor = require("./jobs/payoutProcessor"); -const RentalStatusJob = require("./jobs/rentalStatusJob"); const ConditionCheckReminderJob = require("./jobs/conditionCheckReminder"); const emailServices = require("./services/email"); const s3Service = require("./services/s3Service"); @@ -230,10 +229,6 @@ sequelize const payoutJobs = PayoutProcessor.startScheduledPayouts(); logger.info("Payout processor started"); - // Start the rental status job - const rentalStatusJobs = RentalStatusJob.startScheduledStatusUpdates(); - logger.info("Rental status job started"); - // Start the condition check reminder job const conditionCheckJobs = ConditionCheckReminderJob.startScheduledReminders(); diff --git a/backend/services/conditionCheckService.js b/backend/services/conditionCheckService.js index 12773c8..d4fefa8 100644 --- a/backend/services/conditionCheckService.js +++ b/backend/services/conditionCheckService.js @@ -1,5 +1,6 @@ const { ConditionCheck, Rental, User } = require("../models"); const { Op } = require("sequelize"); +const { isActive } = require("../utils/rentalStatus"); class ConditionCheckService { /** @@ -70,7 +71,7 @@ class ConditionCheckService { canSubmit = now >= timeWindow.start && now <= timeWindow.end && - rental.status === "active"; + isActive(rental); break; case "rental_end_renter": @@ -80,7 +81,7 @@ class ConditionCheckService { canSubmit = now >= timeWindow.start && now <= timeWindow.end && - rental.status === "active"; + isActive(rental); break; case "post_rental_owner": diff --git a/backend/services/damageAssessmentService.js b/backend/services/damageAssessmentService.js index 0b09fc8..49d11de 100644 --- a/backend/services/damageAssessmentService.js +++ b/backend/services/damageAssessmentService.js @@ -1,6 +1,7 @@ const { Rental, Item, ConditionCheck, User } = require("../models"); const LateReturnService = require("./lateReturnService"); const emailServices = require("./email"); +const { isActive } = require("../utils/rentalStatus"); class DamageAssessmentService { /** @@ -34,7 +35,7 @@ class DamageAssessmentService { throw new Error("Only the item owner can report damage"); } - if (rental.status !== "active") { + if (!isActive(rental)) { throw new Error("Can only assess damage for active rentals"); } diff --git a/backend/services/lateReturnService.js b/backend/services/lateReturnService.js index bb315fe..1429c88 100644 --- a/backend/services/lateReturnService.js +++ b/backend/services/lateReturnService.js @@ -1,5 +1,6 @@ const { Rental, Item, User } = require("../models"); const emailServices = require("./email"); +const { isActive } = require("../utils/rentalStatus"); class LateReturnService { /** @@ -71,7 +72,7 @@ class LateReturnService { throw new Error("Rental not found"); } - if (rental.status !== "active") { + if (!isActive(rental)) { throw new Error("Can only process late returns for active rentals"); } diff --git a/backend/services/refundService.js b/backend/services/refundService.js index b9d4a33..49a86f3 100644 --- a/backend/services/refundService.js +++ b/backend/services/refundService.js @@ -1,5 +1,6 @@ const { Rental } = require("../models"); const StripeService = require("./stripeService"); +const { isActive } = require("../utils/rentalStatus"); class RefundService { /** @@ -69,8 +70,8 @@ class RefundService { }; } - // Check if rental is active - if (rental.status === "active") { + // Check if rental is active (computed from confirmed + start time passed) + if (isActive(rental)) { return { canCancel: false, reason: "Cannot cancel active rental", diff --git a/backend/tests/unit/routes/rentals.test.js b/backend/tests/unit/routes/rentals.test.js index 39958a6..c3bcaa6 100644 --- a/backend/tests/unit/routes/rentals.test.js +++ b/backend/tests/unit/routes/rentals.test.js @@ -790,11 +790,15 @@ describe('Rentals Routes', () => { }); describe('POST /:id/mark-completed', () => { + // Active status is computed: confirmed + startDateTime in the past + const pastDate = new Date(); + pastDate.setHours(pastDate.getHours() - 1); // 1 hour ago const mockRental = { id: 1, ownerId: 1, renterId: 2, - status: 'active', + status: 'confirmed', + startDateTime: pastDate, update: jest.fn(), }; @@ -837,7 +841,7 @@ describe('Rentals Routes', () => { expect(response.status).toBe(400); expect(response.body).toEqual({ - error: 'Can only mark active or confirmed rentals as completed', + error: 'Can only mark active rentals as completed', }); }); }); diff --git a/backend/tests/unit/services/conditionCheckService.test.js b/backend/tests/unit/services/conditionCheckService.test.js index ff4f34e..18d7752 100644 --- a/backend/tests/unit/services/conditionCheckService.test.js +++ b/backend/tests/unit/services/conditionCheckService.test.js @@ -10,6 +10,7 @@ describe('ConditionCheckService', () => { describe('submitConditionCheck', () => { // Set rental dates relative to current time for valid time window + // Active status is computed: confirmed + startDateTime in the past const now = new Date(); const mockRental = { id: 'rental-123', @@ -17,7 +18,7 @@ describe('ConditionCheckService', () => { renterId: 'renter-789', startDateTime: new Date(now.getTime() - 1000 * 60 * 60), // 1 hour ago endDateTime: new Date(now.getTime() + 1000 * 60 * 60 * 24), // 24 hours from now - status: 'active' + status: 'confirmed' // Will be computed as "active" since startDateTime is in the past }; const mockPhotos = ['/uploads/photo1.jpg', '/uploads/photo2.jpg']; diff --git a/backend/tests/unit/services/damageAssessmentService.test.js b/backend/tests/unit/services/damageAssessmentService.test.js index 5e3f466..b4e62b6 100644 --- a/backend/tests/unit/services/damageAssessmentService.test.js +++ b/backend/tests/unit/services/damageAssessmentService.test.js @@ -27,11 +27,15 @@ describe('DamageAssessmentService', () => { beforeEach(() => { // Reset mockRental for each test to avoid state pollution + // Active status is computed: confirmed + startDateTime in the past + const pastDate = new Date(); + pastDate.setHours(pastDate.getHours() - 1); // 1 hour ago mockRental = { id: 'rental-123', ownerId: 'owner-789', renterId: 'renter-456', - status: 'active', + status: 'confirmed', + startDateTime: pastDate, item: { name: 'Test Camera', dailyRate: 100 }, update: jest.fn().mockResolvedValue({ id: 'rental-123', diff --git a/backend/tests/unit/services/lateReturnService.test.js b/backend/tests/unit/services/lateReturnService.test.js index f06d98a..f76b063 100644 --- a/backend/tests/unit/services/lateReturnService.test.js +++ b/backend/tests/unit/services/lateReturnService.test.js @@ -91,9 +91,11 @@ describe('LateReturnService', () => { let mockRental; beforeEach(() => { + // Active status is computed: confirmed + startDateTime in the past mockRental = { id: '123', - status: 'active', + status: 'confirmed', + startDateTime: new Date('2023-05-01T08:00:00Z'), // In the past endDateTime: new Date('2023-06-01T10:00:00Z'), item: { pricePerHour: 10, name: 'Test Item' }, renterId: 'renter-123', diff --git a/backend/tests/unit/services/refundService.test.js b/backend/tests/unit/services/refundService.test.js index c985936..d0a7205 100644 --- a/backend/tests/unit/services/refundService.test.js +++ b/backend/tests/unit/services/refundService.test.js @@ -229,8 +229,11 @@ describe('RefundService', () => { }); }); - it('should reject cancellation for active rental', () => { - const rental = { ...baseRental, status: 'active' }; + it('should reject cancellation for active rental (computed from confirmed + past start)', () => { + // Active status is now computed: confirmed + startDateTime in the past + const pastDate = new Date(); + pastDate.setHours(pastDate.getHours() - 1); // 1 hour ago + const rental = { ...baseRental, status: 'confirmed', startDateTime: pastDate }; const result = RefundService.validateCancellationEligibility(rental, 100); expect(result).toEqual({ diff --git a/backend/utils/rentalStatus.js b/backend/utils/rentalStatus.js new file mode 100644 index 0000000..d7c717b --- /dev/null +++ b/backend/utils/rentalStatus.js @@ -0,0 +1,41 @@ +/** + * Utility functions for computing rental status on-the-fly. + * The "active" status is computed based on timestamps rather than stored in the database. + * A rental is considered "active" when it has status "confirmed" and startDateTime has passed. + */ + +/** + * Returns the effective/display status of a rental. + * If the rental is "confirmed" and the start time has passed, returns "active". + * Otherwise returns the stored status. + * @param {Object} rental - The rental object with status and startDateTime + * @returns {string} The effective status + */ +function getEffectiveStatus(rental) { + const now = new Date(); + if ( + rental.status === "confirmed" && + new Date(rental.startDateTime) <= now + ) { + return "active"; + } + return rental.status; +} + +/** + * Checks if a rental is currently active. + * A rental is active when it's confirmed and the start time has passed. + * @param {Object} rental - The rental object with status and startDateTime + * @returns {boolean} True if the rental is currently active + */ +function isActive(rental) { + const now = new Date(); + return ( + rental.status === "confirmed" && new Date(rental.startDateTime) <= now + ); +} + +module.exports = { + getEffectiveStatus, + isActive, +}; diff --git a/frontend/src/components/RentalCancellationModal.tsx b/frontend/src/components/RentalCancellationModal.tsx index 2a23a26..4ec3b64 100644 --- a/frontend/src/components/RentalCancellationModal.tsx +++ b/frontend/src/components/RentalCancellationModal.tsx @@ -41,11 +41,13 @@ const RentalCancellationModal: React.FC = ({ setError(null); // Check if rental status allows cancellation before making API call - if (rental.status !== "pending" && rental.status !== "confirmed") { + // Use displayStatus as it includes computed "active" status + const effectiveStatus = rental.displayStatus || rental.status; + if (effectiveStatus !== "pending" && effectiveStatus !== "confirmed") { let errorMessage = "This rental cannot be cancelled"; - if (rental.status === "active") { + if (effectiveStatus === "active") { errorMessage = "Cannot cancel rental - the rental period has already started"; - } else if (rental.status === "completed") { + } else if (effectiveStatus === "completed") { errorMessage = "Cannot cancel rental - the rental has already been completed"; } else if (rental.status === "cancelled") { errorMessage = "This rental has already been cancelled"; diff --git a/frontend/src/pages/Owning.tsx b/frontend/src/pages/Owning.tsx index 08b9f32..6a64bc0 100644 --- a/frontend/src/pages/Owning.tsx +++ b/frontend/src/pages/Owning.tsx @@ -318,13 +318,16 @@ const Owning: React.FC = () => { }; // Filter owner rentals - exclude cancelled (shown in Rental History) + // Use displayStatus for filtering/sorting as it includes computed "active" status const allOwnerRentals = ownerRentals - .filter((r) => ["pending", "confirmed", "active"].includes(r.status)) + .filter((r) => ["pending", "confirmed", "active"].includes(r.displayStatus || r.status)) .sort((a, b) => { const statusOrder = { pending: 0, confirmed: 1, active: 2 }; + const aStatus = a.displayStatus || a.status; + const bStatus = b.displayStatus || b.status; return ( - statusOrder[a.status as keyof typeof statusOrder] - - statusOrder[b.status as keyof typeof statusOrder] + statusOrder[aStatus as keyof typeof statusOrder] - + statusOrder[bStatus as keyof typeof statusOrder] ); }); @@ -401,17 +404,17 @@ const Owning: React.FC = () => {
- {rental.status.charAt(0).toUpperCase() + - rental.status.slice(1)} + {(rental.displayStatus || rental.status).charAt(0).toUpperCase() + + (rental.displayStatus || rental.status).slice(1)}
@@ -512,7 +515,7 @@ const Owning: React.FC = () => { )} - {rental.status === "confirmed" && ( + {(rental.displayStatus || rental.status) === "confirmed" && ( )} - {rental.status === "active" && ( + {(rental.displayStatus || rental.status) === "active" && ( )} - {rental.status === "active" && + {(rental.displayStatus || rental.status) === "active" && !rental.itemRating && !rental.itemReviewSubmittedAt && (