email plus return item statuses
This commit is contained in:
363
backend/services/conditionCheckService.js
Normal file
363
backend/services/conditionCheckService.js
Normal file
@@ -0,0 +1,363 @@
|
||||
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
|
||||
* @param {Object} metadata - Additional metadata (device info, location, etc.)
|
||||
* @returns {Object} - Created condition check
|
||||
*/
|
||||
static async submitConditionCheck(
|
||||
rentalId,
|
||||
checkType,
|
||||
userId,
|
||||
photos = [],
|
||||
notes = null,
|
||||
metadata = {}
|
||||
) {
|
||||
// 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");
|
||||
}
|
||||
|
||||
// Add timestamp and user agent to metadata
|
||||
const enrichedMetadata = {
|
||||
...metadata,
|
||||
submittedAt: new Date().toISOString(),
|
||||
userAgent: metadata.userAgent || "Unknown",
|
||||
ipAddress: metadata.ipAddress || "Unknown",
|
||||
deviceType: metadata.deviceType || "Unknown",
|
||||
};
|
||||
|
||||
const conditionCheck = await ConditionCheck.create({
|
||||
rentalId,
|
||||
checkType,
|
||||
submittedBy: userId,
|
||||
photos,
|
||||
notes,
|
||||
metadata: enrichedMetadata,
|
||||
});
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user