352 lines
9.8 KiB
JavaScript
352 lines
9.8 KiB
JavaScript
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;
|