optimized condition checks

This commit is contained in:
jackiettran
2026-01-06 17:28:20 -05:00
parent 28c0b4976d
commit 1203fb7996
9 changed files with 275 additions and 401 deletions

View File

@@ -7,6 +7,49 @@ const { IMAGE_LIMITS } = require("../config/imageLimits");
const router = express.Router();
// Get condition checks for multiple rentals in a single request (batch)
router.get("/batch", authenticateToken, async (req, res) => {
try {
const { rentalIds } = req.query;
if (!rentalIds) {
return res.json({
success: true,
conditionChecks: [],
});
}
const ids = rentalIds.split(",").filter((id) => id.trim());
if (ids.length === 0) {
return res.json({
success: true,
conditionChecks: [],
});
}
const conditionChecks =
await ConditionCheckService.getConditionChecksForRentals(ids);
res.json({
success: true,
conditionChecks,
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Error fetching batch condition checks", {
error: error.message,
stack: error.stack,
rentalIds: req.query.rentalIds,
});
res.status(500).json({
success: false,
error: "Failed to fetch condition checks",
});
}
});
// Submit a condition check
router.post("/:rentalId", authenticateToken, async (req, res) => {
try {
@@ -20,9 +63,13 @@ router.post("/:rentalId", authenticateToken, async (req, res) => {
: [];
// Validate S3 keys format and folder
const keyValidation = validateS3Keys(imageFilenamesArray, "condition-checks", {
maxKeys: IMAGE_LIMITS.conditionChecks,
});
const keyValidation = validateS3Keys(
imageFilenamesArray,
"condition-checks",
{
maxKeys: IMAGE_LIMITS.conditionChecks,
}
);
if (!keyValidation.valid) {
return res.status(400).json({
success: false,
@@ -69,69 +116,16 @@ router.post("/:rentalId", authenticateToken, async (req, res) => {
}
});
// 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,
stack: error.stack,
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,
stack: error.stack,
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 { rentalIds } = req.query;
const ids = rentalIds ? rentalIds.split(",").filter((id) => id.trim()) : [];
const availableChecks = await ConditionCheckService.getAvailableChecks(
userId
userId,
ids
);
res.json({

View File

@@ -156,13 +156,21 @@ class ConditionCheckService {
}
/**
* Get all condition checks for a rental
* @param {string} rentalId - Rental ID
* Get all condition checks for multiple rentals (batch)
* @param {Array<string>} rentalIds - Array of Rental IDs
* @returns {Array} - Array of condition checks with user info
*/
static async getConditionChecks(rentalId) {
static async getConditionChecksForRentals(rentalIds) {
if (!rentalIds || rentalIds.length === 0) {
return [];
}
const checks = await ConditionCheck.findAll({
where: { rentalId },
where: {
rentalId: {
[Op.in]: rentalIds,
},
},
include: [
{
model: User,
@@ -176,119 +184,24 @@ class ConditionCheckService {
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", "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.imageFilenames.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
* @param {Array<string>} rentalIds - Array of rental IDs to check
* @returns {Array} - Array of available condition checks
*/
static async getAvailableChecks(userId) {
static async getAvailableChecks(userId, rentalIds) {
if (!rentalIds || rentalIds.length === 0) {
return [];
}
const now = new Date();
const twentyFourHours = 24 * 60 * 60 * 1000;
// Find rentals where user is owner or renter
// Find specified rentals where user is owner or renter
const rentals = await Rental.findAll({
where: {
id: { [Op.in]: rentalIds },
[Op.or]: [{ ownerId: userId }, { renterId: userId }],
status: {
[Op.in]: ["confirmed", "active", "completed"],

View File

@@ -11,8 +11,6 @@ jest.mock('../../../middleware/auth', () => ({
jest.mock('../../../services/conditionCheckService', () => ({
submitConditionCheck: jest.fn(),
getConditionChecks: jest.fn(),
getConditionCheckTimeline: jest.fn(),
getAvailableChecks: jest.fn(),
}));
@@ -183,97 +181,8 @@ describe('Condition Check Routes', () => {
});
});
describe('GET /condition-checks/:rentalId', () => {
it('should return condition checks for a rental', async () => {
const mockChecks = [
{
id: 'check-1',
checkType: 'pre_rental',
notes: 'Good condition',
createdAt: '2024-01-01T00:00:00Z',
},
{
id: 'check-2',
checkType: 'post_rental',
notes: 'Minor wear',
createdAt: '2024-01-15T00:00:00Z',
},
];
ConditionCheckService.getConditionChecks.mockResolvedValue(mockChecks);
const response = await request(app)
.get('/condition-checks/rental-123');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.conditionChecks).toHaveLength(2);
expect(response.body.conditionChecks[0].checkType).toBe('pre_rental');
expect(ConditionCheckService.getConditionChecks).toHaveBeenCalledWith('rental-123');
});
it('should return empty array when no checks exist', async () => {
ConditionCheckService.getConditionChecks.mockResolvedValue([]);
const response = await request(app)
.get('/condition-checks/rental-456');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.conditionChecks).toHaveLength(0);
});
it('should handle service errors', async () => {
ConditionCheckService.getConditionChecks.mockRejectedValue(
new Error('Database error')
);
const response = await request(app)
.get('/condition-checks/rental-123');
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Failed to fetch condition checks');
});
});
describe('GET /condition-checks/:rentalId/timeline', () => {
it('should return condition check timeline', async () => {
const mockTimeline = {
rental: { id: 'rental-123', status: 'completed' },
checks: [
{ type: 'pre_rental', status: 'completed', completedAt: '2024-01-01' },
{ type: 'post_rental', status: 'pending', completedAt: null },
],
};
ConditionCheckService.getConditionCheckTimeline.mockResolvedValue(mockTimeline);
const response = await request(app)
.get('/condition-checks/rental-123/timeline');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.timeline).toMatchObject(mockTimeline);
expect(ConditionCheckService.getConditionCheckTimeline).toHaveBeenCalledWith('rental-123');
});
it('should handle service errors', async () => {
ConditionCheckService.getConditionCheckTimeline.mockRejectedValue(
new Error('Rental not found')
);
const response = await request(app)
.get('/condition-checks/rental-123/timeline');
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Rental not found');
});
});
describe('GET /condition-checks', () => {
it('should return available checks for current user', async () => {
it('should return available checks for specified rentals', async () => {
const mockAvailableChecks = [
{
rentalId: 'rental-1',
@@ -292,16 +201,16 @@ describe('Condition Check Routes', () => {
ConditionCheckService.getAvailableChecks.mockResolvedValue(mockAvailableChecks);
const response = await request(app)
.get('/condition-checks');
.get('/condition-checks?rentalIds=rental-1,rental-2');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.availableChecks).toHaveLength(2);
expect(response.body.availableChecks[0].itemName).toBe('Camera');
expect(ConditionCheckService.getAvailableChecks).toHaveBeenCalledWith('user-123');
expect(ConditionCheckService.getAvailableChecks).toHaveBeenCalledWith('user-123', ['rental-1', 'rental-2']);
});
it('should return empty array when no checks available', async () => {
it('should return empty array when no rentalIds provided', async () => {
ConditionCheckService.getAvailableChecks.mockResolvedValue([]);
const response = await request(app)
@@ -310,6 +219,7 @@ describe('Condition Check Routes', () => {
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.availableChecks).toHaveLength(0);
expect(ConditionCheckService.getAvailableChecks).toHaveBeenCalledWith('user-123', []);
});
it('should handle service errors', async () => {
@@ -318,7 +228,7 @@ describe('Condition Check Routes', () => {
);
const response = await request(app)
.get('/condition-checks');
.get('/condition-checks?rentalIds=rental-1');
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);

View File

@@ -122,44 +122,4 @@ describe('ConditionCheckService', () => {
).rejects.toThrow('Rental not found');
});
});
describe('getConditionChecks', () => {
it('should retrieve condition checks for rental', async () => {
const mockChecks = [
{
id: 'check-1',
checkType: 'pre_rental_owner',
submittedBy: 'owner-456',
submittedAt: '2023-05-31T12:00:00Z',
photos: ['/uploads/photo1.jpg'],
notes: 'Item ready'
},
{
id: 'check-2',
checkType: 'rental_start_renter',
submittedBy: 'renter-789',
submittedAt: '2023-06-01T11:00:00Z',
photos: ['/uploads/photo2.jpg'],
notes: 'Item received'
}
];
ConditionCheck.findAll.mockResolvedValue(mockChecks);
const result = await ConditionCheckService.getConditionChecks('rental-123');
expect(ConditionCheck.findAll).toHaveBeenCalledWith({
where: { rentalId: 'rental-123' },
include: [{
model: User,
as: 'submittedByUser',
attributes: ['id', 'firstName', 'lastName']
}],
order: [['submittedAt', 'ASC']]
});
expect(result).toEqual(mockChecks);
expect(result.length).toBe(2);
});
});
});