const { ConditionCheck, Rental, User } = require("../models"); const { Op } = require("sequelize"); class ConditionCheckService { /** * Validate if a condition check can be submitted * @param {string} rentalId - Rental ID * @param {string} checkType - Type of check (pre_rental_owner, rental_start_renter, etc.) * @param {string} userId - User attempting to submit * @returns {Object} - { canSubmit, reason, timeWindow } */ static async validateConditionCheck(rentalId, checkType, userId) { const rental = await Rental.findByPk(rentalId); if (!rental) { return { canSubmit: false, reason: "Rental not found" }; } // Check user permissions const isOwner = rental.ownerId === userId; const isRenter = rental.renterId === userId; if (checkType.includes("owner") && !isOwner) { return { canSubmit: false, reason: "Only the item owner can submit owner condition checks", }; } if (checkType.includes("renter") && !isRenter) { return { canSubmit: false, reason: "Only the renter can submit renter condition checks", }; } // Check if already submitted const existingCheck = await ConditionCheck.findOne({ where: { rentalId, checkType }, }); if (existingCheck) { return { canSubmit: false, reason: "Condition check already submitted for this type", }; } // Check time windows (24 hour windows) const now = new Date(); const startDate = new Date(rental.startDateTime); const endDate = new Date(rental.endDateTime); const twentyFourHours = 24 * 60 * 60 * 1000; let timeWindow = {}; let canSubmit = false; switch (checkType) { case "pre_rental_owner": // 24 hours before rental starts timeWindow.start = new Date(startDate.getTime() - twentyFourHours); timeWindow.end = startDate; canSubmit = now >= timeWindow.start && now <= timeWindow.end; break; case "rental_start_renter": // 24 hours after rental starts timeWindow.start = startDate; timeWindow.end = new Date(startDate.getTime() + twentyFourHours); canSubmit = now >= timeWindow.start && now <= timeWindow.end && rental.status === "active"; break; case "rental_end_renter": // 24 hours before rental ends timeWindow.start = new Date(endDate.getTime() - twentyFourHours); timeWindow.end = endDate; canSubmit = now >= timeWindow.start && now <= timeWindow.end && rental.status === "active"; break; case "post_rental_owner": // Can be submitted anytime (integrated into return flow) timeWindow.start = endDate; timeWindow.end = null; // No time limit canSubmit = true; // Always allowed when owner marks return break; default: return { canSubmit: false, reason: "Invalid check type" }; } if (!canSubmit) { const isBeforeWindow = now < timeWindow.start; const isAfterWindow = now > timeWindow.end; let reason = "Outside of allowed time window"; if (isBeforeWindow) { reason = `Too early. Check can be submitted starting ${timeWindow.start.toLocaleString()}`; } else if (isAfterWindow) { reason = `Pre-Rental Condition can only be submitted before start of rental period`; } return { canSubmit: false, reason, timeWindow }; } return { canSubmit: true, timeWindow }; } /** * Submit a condition check with photos * @param {string} rentalId - Rental ID * @param {string} checkType - Type of check * @param {string} userId - User submitting the check * @param {Array} photos - Array of photo URLs * @param {string} notes - Optional notes * @returns {Object} - Created condition check */ static async submitConditionCheck( rentalId, checkType, userId, photos = [], notes = null ) { // Validate the check const validation = await this.validateConditionCheck( rentalId, checkType, userId ); if (!validation.canSubmit) { throw new Error(validation.reason); } // Validate photos (basic validation) if (photos.length > 20) { throw new Error("Maximum 20 photos allowed per condition check"); } const conditionCheck = await ConditionCheck.create({ rentalId, checkType, submittedBy: userId, photos, notes, }); return conditionCheck; } /** * Get all condition checks for a rental * @param {string} rentalId - Rental ID * @returns {Array} - Array of condition checks with user info */ static async getConditionChecks(rentalId) { const checks = await ConditionCheck.findAll({ where: { rentalId }, include: [ { model: User, as: "submittedByUser", attributes: ["id", "username", "firstName", "lastName"], }, ], order: [["submittedAt", "ASC"]], }); return checks; } /** * Get condition check timeline for a rental * @param {string} rentalId - Rental ID * @returns {Object} - Timeline showing what checks are available/completed */ static async getConditionCheckTimeline(rentalId) { const rental = await Rental.findByPk(rentalId); if (!rental) { throw new Error("Rental not found"); } const existingChecks = await ConditionCheck.findAll({ where: { rentalId }, include: [ { model: User, as: "submittedByUser", attributes: ["id", "username", "firstName", "lastName"], }, ], }); const checkTypes = [ "pre_rental_owner", "rental_start_renter", "rental_end_renter", "post_rental_owner", ]; const timeline = {}; for (const checkType of checkTypes) { const existingCheck = existingChecks.find( (check) => check.checkType === checkType ); if (existingCheck) { timeline[checkType] = { status: "completed", submittedAt: existingCheck.submittedAt, submittedBy: existingCheck.submittedBy, photoCount: existingCheck.photos.length, hasNotes: !!existingCheck.notes, }; } else { // Calculate if this check type is available const now = new Date(); const startDate = new Date(rental.startDateTime); const endDate = new Date(rental.endDateTime); const twentyFourHours = 24 * 60 * 60 * 1000; let timeWindow = {}; let status = "not_available"; switch (checkType) { case "pre_rental_owner": timeWindow.start = new Date(startDate.getTime() - twentyFourHours); timeWindow.end = startDate; break; case "rental_start_renter": timeWindow.start = startDate; timeWindow.end = new Date(startDate.getTime() + twentyFourHours); break; case "rental_end_renter": timeWindow.start = new Date(endDate.getTime() - twentyFourHours); timeWindow.end = endDate; break; case "post_rental_owner": timeWindow.start = endDate; timeWindow.end = new Date(endDate.getTime() + twentyFourHours); break; } if (now >= timeWindow.start && now <= timeWindow.end) { status = "available"; } else if (now < timeWindow.start) { status = "pending"; } else { status = "expired"; } timeline[checkType] = { status, timeWindow, availableFrom: timeWindow.start, availableUntil: timeWindow.end, }; } } return { rental: { id: rental.id, startDateTime: rental.startDateTime, endDateTime: rental.endDateTime, status: rental.status, }, timeline, }; } /** * Get available condition checks for a user * @param {string} userId - User ID * @returns {Array} - Array of available condition checks */ static async getAvailableChecks(userId) { const now = new Date(); const twentyFourHours = 24 * 60 * 60 * 1000; // Find rentals where user is owner or renter const rentals = await Rental.findAll({ where: { [Op.or]: [{ ownerId: userId }, { renterId: userId }], status: { [Op.in]: ["confirmed", "active", "completed"], }, }, }); const availableChecks = []; for (const rental of rentals) { const isOwner = rental.ownerId === userId; const isRenter = rental.renterId === userId; const startDate = new Date(rental.startDateTime); const endDate = new Date(rental.endDateTime); // Check each type of condition check const checkTypes = []; if (isOwner) { // Only include pre_rental_owner; post_rental is now part of return flow checkTypes.push("pre_rental_owner"); } if (isRenter) { checkTypes.push("rental_start_renter", "rental_end_renter"); } for (const checkType of checkTypes) { // Check if already submitted const existing = await ConditionCheck.findOne({ where: { rentalId: rental.id, checkType }, }); if (!existing) { const validation = await this.validateConditionCheck( rental.id, checkType, userId ); if (validation.canSubmit) { availableChecks.push({ rentalId: rental.id, checkType, rental: { id: rental.id, itemId: rental.itemId, startDateTime: rental.startDateTime, endDateTime: rental.endDateTime, }, timeWindow: validation.timeWindow, }); } } } } return availableChecks; } } module.exports = ConditionCheckService;