From 42dbb375650e13042e7ee4761202db2680a902ae Mon Sep 17 00:00:00 2001 From: Amaresh Chaudhary Nara Date: Sat, 20 Jun 2026 12:00:18 -0700 Subject: [PATCH 1/7] feat(issues): add most-expensive endpoint and fix longest-open params --- .../bmdashboard/bmIssueController.js | 104 +++++++++++------- src/routes/bmdashboard/bmIssueRouter.js | 1 + 2 files changed, 66 insertions(+), 39 deletions(-) diff --git a/src/controllers/bmdashboard/bmIssueController.js b/src/controllers/bmdashboard/bmIssueController.js index 791ed459c9..529cde53db 100644 --- a/src/controllers/bmdashboard/bmIssueController.js +++ b/src/controllers/bmdashboard/bmIssueController.js @@ -256,56 +256,81 @@ const bmIssueController = function (BuildingIssue, injuryIssue) { } }; - /* -------------------- LONGEST OPEN ISSUES (FINAL) -------------------- */ + /* -------------------- LONGEST OPEN ISSUES -------------------- */ const getLongestOpenIssues = async (req, res) => { try { - const { dates, projects } = req.query; + const { projectIds, startDate, endDate } = req.query; const query = { status: 'open' }; - let filteredProjectIds = getProjectFilterIds(projects); - filteredProjectIds = await filterProjectIdsByDates(dates, filteredProjectIds); + if (projectIds) { + const ids = projectIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean); + if (ids.length > 0) query.projectId = { $in: ids }; + } - if (dates && filteredProjectIds.length === 0) { - return res.json([]); + if (startDate || endDate) { + query.issueDate = {}; + if (startDate) query.issueDate.$gte = new Date(startDate); + if (endDate) query.issueDate.$lte = new Date(endDate); } - if (filteredProjectIds.length) { - query.projectId = { $in: filteredProjectIds }; + const today = new Date(); + const issues = await BuildingIssue.find(query).select('issueTitle issueDate').lean(); + + const result = issues + .map((issue) => ({ + issueId: issue._id, + title: Array.isArray(issue.issueTitle) ? issue.issueTitle[0] : issue.issueTitle, + daysOpen: Math.floor((today - new Date(issue.issueDate)) / (1000 * 60 * 60 * 24)), + })) + .sort((a, b) => b.daysOpen - a.daysOpen) + .slice(0, 5); + + return res.json({ data: result }); + } catch (error) { + return res.status(500).json({ message: 'Error fetching longest open issues' }); + } + }; + + /* -------------------- MOST EXPENSIVE ISSUES -------------------- */ + const getMostExpensiveIssues = async (req, res) => { + try { + const { projectIds, startDate, endDate } = req.query; + const query = {}; + + if (projectIds) { + const ids = projectIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean); + if (ids.length > 0) query.projectId = { $in: ids }; } - let issues = await BuildingIssue.find(query) - .select('issueTitle issueDate _id') - .populate('projectId') - .lean(); - - issues = issues.map((issue) => { - const durationInMonths = getDurationOpenMonths(issue.issueDate); - return { - issueName: issue.issueTitle && issue.issueTitle.length > 0 ? issue.issueTitle[0] : null, - durationInMonths, - issueId: issue._id.toString(), - projectId: issue.projectId?._id?.toString() || issue.projectId?.toString(), - projectName: issue.projectId?.name || null, - }; - }); - - const sortedIssues = issues - .sort((a, b) => b.durationInMonths - a.durationInMonths) - .map(({ issueName, durationInMonths, issueId, projectId, projectName }) => ({ - issueName, - durationOpen: durationInMonths, - issueId, - projectId, - projectName, - })); - - console.log( - `[getLongestOpenIssues] Total issues found: ${issues.length}, Returning: ${sortedIssues.length} issues`, - ); + if (startDate || endDate) { + query.openDate = {}; + if (startDate) query.openDate.$gte = new Date(startDate); + if (endDate) query.openDate.$lte = new Date(endDate); + } - res.json(sortedIssues); + const today = new Date(); + const issues = await injuryIssue.find(query).select('name openDate totalCost').lean(); + + const result = issues + .filter((issue) => issue.totalCost != null) + .map((issue) => ({ + issueId: issue._id, + title: issue.name, + totalCost: issue.totalCost, + daysOpen: Math.floor((today - new Date(issue.openDate)) / (1000 * 60 * 60 * 24)), + })) + .sort((a, b) => b.totalCost - a.totalCost) + .slice(0, 5); + + return res.json({ data: result }); } catch (error) { - res.status(500).json({ message: 'Error fetching longest open issues' }); + return res.status(500).json({ message: 'Error fetching most expensive issues' }); } }; @@ -314,6 +339,7 @@ const bmIssueController = function (BuildingIssue, injuryIssue) { bmPostIssue, bmGetIssueChart, getLongestOpenIssues, + getMostExpensiveIssues, bmPostInjuryIssue, bmGetInjuryIssue, bmDeleteInjuryIssue, diff --git a/src/routes/bmdashboard/bmIssueRouter.js b/src/routes/bmdashboard/bmIssueRouter.js index 394e87303d..994a8bcac3 100644 --- a/src/routes/bmdashboard/bmIssueRouter.js +++ b/src/routes/bmdashboard/bmIssueRouter.js @@ -13,6 +13,7 @@ const routes = function (buildingIssue, injuryIssue) { IssueRouter.route('/issues/add').post(controller.bmPostInjuryIssue); IssueRouter.route('/issues/list').get(controller.bmGetInjuryIssue); IssueRouter.route('/issues/longest-open').get(controller.getLongestOpenIssues); + IssueRouter.route('/issues/most-expensive').get(controller.getMostExpensiveIssues); IssueRouter.route('/issues/:id/rename').put(controller.bmRenameInjuryIssue); IssueRouter.route('/issues/:id/copy').post(controller.bmCopyInjuryIssue); IssueRouter.route('/issues/:id').delete(controller.bmDeleteInjuryIssue); From c2369e6a9d23c7d6596851e08d8563a010105ce4 Mon Sep 17 00:00:00 2001 From: Amaresh Chaudhary Nara Date: Tue, 30 Jun 2026 18:18:36 -0700 Subject: [PATCH 2/7] fix: point injuryIssue.projectId ref at buildingProject instead of Project --- src/models/bmdashboard/injuryIssue.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/bmdashboard/injuryIssue.js b/src/models/bmdashboard/injuryIssue.js index b24b4f41b5..dee5dd4e8b 100644 --- a/src/models/bmdashboard/injuryIssue.js +++ b/src/models/bmdashboard/injuryIssue.js @@ -3,7 +3,7 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; const injuryIssue = new Schema({ - projectId: { type: mongoose.SchemaTypes.ObjectId, ref: 'Project', required: true }, + projectId: { type: mongoose.SchemaTypes.ObjectId, ref: 'buildingProject', required: true }, name: { type: String, required: true }, openDate: { type: Date, default: Date.now }, category: { type: String, required: true }, From e3330aacbcb489a3fad6504dd2173ffbc91e1762 Mon Sep 17 00:00:00 2001 From: Gayatri Sawant Date: Thu, 20 Aug 2026 12:13:26 -0400 Subject: [PATCH 3/7] Fix total volunteer count mismatch by aligning base filters in getTeamMembersCount --- src/helpers/overviewReportHelper.js | 198 ++++++++-------------------- 1 file changed, 56 insertions(+), 142 deletions(-) diff --git a/src/helpers/overviewReportHelper.js b/src/helpers/overviewReportHelper.js index 8b8bdb32ad..e92c55d6e2 100644 --- a/src/helpers/overviewReportHelper.js +++ b/src/helpers/overviewReportHelper.js @@ -897,12 +897,6 @@ const overviewReportHelper = function () { const getData = async (endDate) => { const baseFilters = { isActive: true, - weeklycommittedHours: { - $gte: 1, - }, - role: { - $ne: 'Mentor', - }, }; if (endDate) { @@ -1187,80 +1181,6 @@ const overviewReportHelper = function () { }; } - /** - * Get the volunteer hours stats, it retrieves the number of hours logged by users between the two input dates as well as their weeklycommittedHours. - * @param {*} startDate - * @param {*} endDate - */ - // async function getHoursStats(startDate, endDate) { - // const hoursStats = await UserProfile.aggregate([ - // { - // $match: { - // isActive: true, - // }, - // }, - // { - // $lookup: { - // from: 'timeEntries', // The collection to join - // localField: '_id', // Field from the userProfile collection - // foreignField: 'personId', // Field from the timeEntries collection - // as: 'timeEntries', // The array field that will contain the joined documents - // }, - // }, - // { - // $unwind: { - // path: '$timeEntries', - // preserveNullAndEmptyArrays: true, // Preserve users with no time entries - // }, - // }, - // { - // $match: { - // $or: [ - // { timeEntries: { $exists: false } }, - // { - // 'timeEntries.dateOfWork': { - // $gte: moment(startDate).format('YYYY-MM-DD'), - // $lte: moment(endDate).format('YYYY-MM-DD'), - // }, - // }, - // ], - // }, - // }, - // { - // $group: { - // _id: '$_id', - // personId: { $first: '$_id' }, - // totalSeconds: { $sum: '$timeEntries.totalSeconds' }, // Sum seconds from timeEntries - // weeklycommittedHours: { $first: `$weeklycommittedHours` }, // Include the weeklycommittedHours field - // }, - // }, - // { - // $project: { - // totalHours: { $divide: ['$totalSeconds', 3600] }, // Convert seconds to hours - // weeklycommittedHours: 1, // make sure we include it in the end result - // }, - // }, - // { - // $bucket: { - // groupBy: '$totalHours', - // boundaries: [0, 10, 20, 30, 40], - // default: 40, - // output: { - // count: { $sum: 1 }, - // }, - // }, - // }, - // ]); - - // for (let i = 0; i < 5; i++) { - // if (!hoursStats.find((x) => x._id === i * 10)) { - // hoursStats.push({ _id: i * 10, count: 0 }); - // } - // } - - // return hoursStats; - // } - /** * Helper function to get user-level hours data for internal use * This is used by getVolunteerHoursStats for percentage calculations @@ -1493,15 +1413,18 @@ const overviewReportHelper = function () { } /** - * Aggregates total hours worked this week across all active volunteers, - * matching the dashboard's getOrgData logic exactly: - * - Current week (America/Los_Angeles) date range, ignoring any passed-in date filters + * Aggregates total hours worked across active volunteers within the provided date range. * - Only active users with weeklycommittedHours >= 1 and role != Mentor * - Excludes entryType of 'person', 'team', or 'project' */ - async function getTotalHoursWorked() { - const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); - const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); + async function getTotalHoursWorked(isoStartDate, isoEndDate) { + const pdtstart = isoStartDate + ? moment(isoStartDate).tz('America/Los_Angeles').format('YYYY-MM-DD') + : moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); + + const pdtend = isoEndDate + ? moment(isoEndDate).tz('America/Los_Angeles').format('YYYY-MM-DD') + : moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); const data = await UserProfile.aggregate([ { @@ -2546,12 +2469,16 @@ const overviewReportHelper = function () { isoComparisonStartDate, isoComparisonEndDate, ) { - // Helper function to get count of volunteers meeting their commitment. const getCompletedHoursData = async (start, end) => { + const startStr = moment(start).tz('America/Los_Angeles').format('YYYY-MM-DD'); + const endStr = moment(end).tz('America/Los_Angeles').format('YYYY-MM-DD'); + const hoursStats = await UserProfile.aggregate([ { $match: { isActive: true, + weeklycommittedHours: { $gte: 1 }, + role: { $ne: 'Mentor' }, }, }, { @@ -2573,12 +2500,9 @@ const overviewReportHelper = function () { as: 'entry', cond: { $and: [ - { - $gte: ['$$entry.dateOfWork', moment(start).format('YYYY-MM-DD')], - }, - { - $lte: ['$$entry.dateOfWork', moment(end).format('YYYY-MM-DD')], - }, + { $gte: ['$$entry.dateOfWork', startStr] }, + { $lte: ['$$entry.dateOfWork', endStr] }, + { $not: [{ $in: ['$$entry.entryType', ['person', 'team', 'project']] }] }, ], }, }, @@ -2592,7 +2516,6 @@ const overviewReportHelper = function () { }, { $project: { - name: { $concat: ['$firstName', ' ', '$lastName'] }, totalHours: { $divide: ['$totalSeconds', 3600] }, weeklycommittedHours: 1, metCommitment: { @@ -2605,13 +2528,14 @@ const overviewReportHelper = function () { }, }, { - $match: { weeklycommittedHours: { $gte: 1 } }, + $match: { metCommitment: true }, + }, + { + $count: 'metCommitmentCount', }, ]); - const metCommitment = hoursStats.filter((user) => user.metCommitment); - const metCommitmentCount = metCommitment.length; - return metCommitmentCount; + return hoursStats[0]?.metCommitmentCount || 0; }; const currentCount = await getCompletedHoursData(isoStartDate, isoEndDate); @@ -2638,61 +2562,52 @@ const overviewReportHelper = function () { comparisonStartDate, comparisonEndDate, ) => { - // Helper function to count summaries submitted within a date range - const getSummariesCount = async (start, end) => - UserProfile.aggregate([ + const getSummariesCount = async (start, end) => { + const startDateObj = new Date(moment(start).tz('America/Los_Angeles').startOf('day').toISOString()); + const endDateObj = new Date(moment(end).tz('America/Los_Angeles').endOf('day').toISOString()); + + const result = await UserProfile.aggregate([ { - // Stage 1: Match users who have weeklySummaries array $match: { - weeklySummaries: { $exists: true, $ne: [] }, - }, + $or: [ + { weeklySummaries: { $exists: true, $ne: [] } }, + { summaries: { $exists: true, $ne: [] } } + ] + } }, { - // Stage 2: Project only the summaries that meet ALL criteria - $project: { - validSummaries: { - $filter: { - input: '$weeklySummaries', - as: 'summary', - cond: { - $and: [ - // Condition 1: Summary content is not empty - { $ne: ['$$summary.summary', ''] }, - { $ne: ['$$summary.summary', null] }, - // Condition 2: uploadDate field exists - { $ne: ['$$summary.uploadDate', null] }, - // Condition 3: uploadDate is within the date range - { $gte: ['$$summary.uploadDate', new Date(start)] }, - { $lte: ['$$summary.uploadDate', new Date(end)] }, - ], - }, - }, - }, - }, + $addFields: { + allSummaries: { $concatArrays: [{ $ifNull: ['$weeklySummaries', []] }, { $ifNull: ['$summaries', []] }] } + } }, + { $unwind: '$allSummaries' }, { - // Stage 3: Count the valid summaries for each user - $project: { - summaryCount: { $size: '$validSummaries' }, - }, + $addFields: { + summaryParsedDate: { + $cond: { + if: { $ne: ['$allSummaries.uploadDate', null] }, + then: { $toDate: '$allSummaries.uploadDate' }, + else: { $toDate: '$allSummaries.date' } + } + } + } }, { - // Stage 4: Sum across all users - $group: { - _id: null, - totalSummaries: { $sum: '$summaryCount' }, - }, + $match: { + 'allSummaries.summary': { $exists: true, $ne: '', $ne: null }, + summaryParsedDate: { $gte: startDateObj, $lte: endDateObj } + } }, + { $count: 'totalSummaries' } ]); - // Get summaries count for the current date range - const currentSummaries = await getSummariesCount(startDate, endDate); - const totalCurrentSummaries = currentSummaries[0]?.totalSummaries || 0; + return result[0]?.totalSummaries || 0; + }; + + const totalCurrentSummaries = await getSummariesCount(startDate, endDate); - // If comparison dates are provided, calculate the comparison percentage if (comparisonStartDate && comparisonEndDate) { - const comparisonSummaries = await getSummariesCount(comparisonStartDate, comparisonEndDate); - const totalComparisonSummaries = comparisonSummaries[0]?.totalSummaries || 0; + const totalComparisonSummaries = await getSummariesCount(comparisonStartDate, comparisonEndDate); const comparisonPercentage = calculateGrowthPercentage( totalCurrentSummaries, totalComparisonSummaries, @@ -2701,7 +2616,6 @@ const overviewReportHelper = function () { return { count: totalCurrentSummaries, comparisonPercentage }; } - // If no comparison dates, return only the count return { count: totalCurrentSummaries }; }; @@ -2735,4 +2649,4 @@ const overviewReportHelper = function () { }; }; -module.exports = overviewReportHelper; +module.exports = overviewReportHelper; \ No newline at end of file From 63b616bd6442684922ed8f6fd646b073d2369f14 Mon Sep 17 00:00:00 2001 From: Gayatri Sawant Date: Thu, 27 Aug 2026 22:53:17 -0400 Subject: [PATCH 4/7] Fix duplicate $ne key in getSummariesCount aggregation causing lint failure --- src/helpers/overviewReportHelper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/overviewReportHelper.js b/src/helpers/overviewReportHelper.js index 9dd5c1fd2a..74b677d991 100644 --- a/src/helpers/overviewReportHelper.js +++ b/src/helpers/overviewReportHelper.js @@ -2581,7 +2581,7 @@ const overviewReportHelper = function () { }, { $match: { - 'allSummaries.summary': { $exists: true, $ne: '', $ne: null }, + 'allSummaries.summary': { $exists: true, $nin: ['', null] }, summaryParsedDate: { $gte: startDateObj, $lte: endDateObj } } }, From 81e3678e7d65c31270adc7c88d1810b558e57248 Mon Sep 17 00:00:00 2001 From: Gayatri Sawant Date: Thu, 27 Aug 2026 23:29:15 -0400 Subject: [PATCH 5/7] Update getLongestOpenIssues tests to match new projectIds/startDate/endDate query params and direct issueDate filtering behavior --- .../__tests__/bmIssueController.test.js | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/controllers/bmdashboard/__tests__/bmIssueController.test.js b/src/controllers/bmdashboard/__tests__/bmIssueController.test.js index 75d9b8b5f3..137ee246c0 100644 --- a/src/controllers/bmdashboard/__tests__/bmIssueController.test.js +++ b/src/controllers/bmdashboard/__tests__/bmIssueController.test.js @@ -796,8 +796,8 @@ describe('Building Issue Controller', () => { expect(result[0].durationOpen).toBeGreaterThan(0); }); - it('should filter by projects when provided', async () => { - req.query.projects = TEST_ISSUE_ID; + it('should filter by projectIds when provided', async () => { + req.query.projectIds = TEST_ISSUE_ID; mockBuildingIssue.find.mockReturnValue(mockFindChain([])); @@ -807,34 +807,34 @@ describe('Building Issue Controller', () => { expect(callArgs.projectId.$in).toContain(TEST_ISSUE_ID); }); - it('should filter by dates and return matching projects', async () => { - req.query.dates = '2022-01-01,2024-12-31'; - mockBuildingProjectFind.mockReturnValue(mockBuildingProjectChain([{ _id: TEST_ISSUE_ID }])); + it('should filter by startDate and endDate when provided', async () => { + req.query.startDate = '2022-01-01'; + req.query.endDate = '2024-12-31'; mockBuildingIssue.find.mockReturnValue(mockFindChain([])); await controller.getLongestOpenIssues(req, res); - expect(mockBuildingProjectFind).toHaveBeenCalled(); + const callArgs = mockBuildingIssue.find.mock.calls[0][0]; + expect(callArgs.issueDate.$gte).toBeInstanceOf(Date); + expect(callArgs.issueDate.$lte).toBeInstanceOf(Date); expect(res.json).toHaveBeenCalled(); }); - it('should return empty array when dates provided but no matching projects', async () => { req.query.dates = '2022-01-01,2024-12-31'; mockBuildingProjectFind.mockReturnValue(mockBuildingProjectChain([])); await controller.getLongestOpenIssues(req, res); expect(res.json).toHaveBeenCalledWith([]); }); - - it('should intersect project filters when both dates and projects provided', async () => { + it('should apply both projectIds and date filters when provided', async () => { const anotherProjectId = '507f1f77bcf86cd799439012'; - req.query.dates = '2022-01-01,2024-12-31'; - req.query.projects = `${TEST_ISSUE_ID},${anotherProjectId}`; - mockBuildingProjectFind.mockReturnValue(mockBuildingProjectChain([{ _id: TEST_ISSUE_ID }])); + req.query.startDate = '2022-01-01'; + req.query.endDate = '2024-12-31'; + req.query.projectIds = `${TEST_ISSUE_ID},${anotherProjectId}`; mockBuildingIssue.find.mockReturnValue(mockFindChain([])); await controller.getLongestOpenIssues(req, res); - expect(mockBuildingProjectFind).toHaveBeenCalled(); const callArgs = mockBuildingIssue.find.mock.calls[0][0]; expect(callArgs.projectId.$in).toContain(TEST_ISSUE_ID); + expect(callArgs.issueDate.$gte).toBeInstanceOf(Date); + expect(callArgs.issueDate.$lte).toBeInstanceOf(Date); }); - it('should return top 7 issues sorted by duration', async () => { const mockIssues = Array.from({ length: 10 }, (_, i) => ({ issueTitle: [`Issue ${i + 1}`], From f4dd95ebb1ad9224d039965e4937878f585f834a Mon Sep 17 00:00:00 2001 From: Gayatri Sawant Date: Fri, 28 Aug 2026 00:07:50 -0400 Subject: [PATCH 6/7] Add test coverage for getMostExpensiveIssues to address SonarCloud Quality Gate coverage requirement --- .../__tests__/bmIssueController.test.js | 173 +++++++++++++++++- 1 file changed, 169 insertions(+), 4 deletions(-) diff --git a/src/controllers/bmdashboard/__tests__/bmIssueController.test.js b/src/controllers/bmdashboard/__tests__/bmIssueController.test.js index 137ee246c0..ced8f2efa9 100644 --- a/src/controllers/bmdashboard/__tests__/bmIssueController.test.js +++ b/src/controllers/bmdashboard/__tests__/bmIssueController.test.js @@ -31,6 +31,17 @@ const mockBuildingIssue = { findByIdAndDelete: jest.fn(), }; +// Mocking the injuryIssue Model (used by getMostExpensiveIssues) +const mockInjuryIssue = { + find: jest.fn(), +}; + +// Helper: builds the chained find mock used by getMostExpensiveIssues +const mockInjuryFindChain = (result) => ({ + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockResolvedValue(result), +}); + // Helper: call bmGetOpenIssue and return the find query that was used (for filter tests) async function getOpenIssueFindQuery(controller, req, res, findResult = []) { mockBuildingIssue.find.mockResolvedValue(findResult); @@ -57,7 +68,7 @@ describe('Building Issue Controller', () => { let res; beforeEach(() => { - controller = bmIssueController(mockBuildingIssue); + controller = bmIssueController(mockBuildingIssue, mockInjuryIssue); req = { body: {}, @@ -796,7 +807,7 @@ describe('Building Issue Controller', () => { expect(result[0].durationOpen).toBeGreaterThan(0); }); - it('should filter by projectIds when provided', async () => { + it('should filter by projectIds when provided', async () => { req.query.projectIds = TEST_ISSUE_ID; mockBuildingIssue.find.mockReturnValue(mockFindChain([])); @@ -807,7 +818,7 @@ describe('Building Issue Controller', () => { expect(callArgs.projectId.$in).toContain(TEST_ISSUE_ID); }); - it('should filter by startDate and endDate when provided', async () => { + it('should filter by startDate and endDate when provided', async () => { req.query.startDate = '2022-01-01'; req.query.endDate = '2024-12-31'; mockBuildingIssue.find.mockReturnValue(mockFindChain([])); @@ -893,4 +904,158 @@ describe('Building Issue Controller', () => { expect(res.json).toHaveBeenCalledWith({ message: 'Error fetching longest open issues' }); }); }); -}); + + // ==================== getMostExpensiveIssues Tests ==================== + describe('getMostExpensiveIssues', () => { + beforeEach(() => { + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + }); + + it('should fetch most expensive issues without filters', async () => { + const mockIssues = [ + { _id: '1', name: 'Cheap Issue', openDate: new Date('2024-01-01'), totalCost: 100 }, + { _id: '2', name: 'Expensive Issue', openDate: new Date('2024-01-01'), totalCost: 5000 }, + { _id: '3', name: 'Medium Issue', openDate: new Date('2024-01-01'), totalCost: 1000 }, + ]; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain(mockIssues)); + + await controller.getMostExpensiveIssues(req, res); + + expect(mockInjuryIssue.find).toHaveBeenCalledWith({}); + expect(res.json).toHaveBeenCalled(); + const { data } = res.json.mock.calls[0][0]; + expect(data).toHaveLength(3); + // sorted descending by totalCost + expect(data[0].totalCost).toBe(5000); + expect(data[1].totalCost).toBe(1000); + expect(data[2].totalCost).toBe(100); + expect(data[0].title).toBe('Expensive Issue'); + expect(data[0].issueId).toBe('2'); + expect(typeof data[0].daysOpen).toBe('number'); + }); + + it('should filter by projectIds when provided', async () => { + const validId2 = '507f1f77bcf86cd799439012'; + req.query.projectIds = `${TEST_ISSUE_ID},${validId2}`; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.projectId.$in).toHaveLength(2); + expect(callArgs.projectId.$in).toContain(TEST_ISSUE_ID); + }); + + it('should not add projectId filter when projectIds is empty after trimming', async () => { + req.query.projectIds = ' , '; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.projectId).toBeUndefined(); + }); + + it('should filter by startDate and endDate when both provided', async () => { + req.query.startDate = '2024-01-01'; + req.query.endDate = '2024-12-31'; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.openDate.$gte).toBeInstanceOf(Date); + expect(callArgs.openDate.$lte).toBeInstanceOf(Date); + }); + + it('should filter by startDate only when endDate not provided', async () => { + req.query.startDate = '2024-01-01'; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.openDate.$gte).toBeInstanceOf(Date); + expect(callArgs.openDate.$lte).toBeUndefined(); + }); + + it('should filter by endDate only when startDate not provided', async () => { + req.query.endDate = '2024-12-31'; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.openDate.$lte).toBeInstanceOf(Date); + expect(callArgs.openDate.$gte).toBeUndefined(); + }); + + it('should apply both projectIds and date filters when provided', async () => { + req.query.projectIds = TEST_ISSUE_ID; + req.query.startDate = '2024-01-01'; + req.query.endDate = '2024-12-31'; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.projectId.$in).toContain(TEST_ISSUE_ID); + expect(callArgs.openDate.$gte).toBeInstanceOf(Date); + expect(callArgs.openDate.$lte).toBeInstanceOf(Date); + }); + + it('should exclude issues with null or undefined totalCost', async () => { + const mockIssues = [ + { _id: '1', name: 'No Cost', openDate: new Date('2024-01-01'), totalCost: null }, + { _id: '2', name: 'Undefined Cost', openDate: new Date('2024-01-01') }, + { _id: '3', name: 'Has Cost', openDate: new Date('2024-01-01'), totalCost: 500 }, + ]; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain(mockIssues)); + + await controller.getMostExpensiveIssues(req, res); + + const { data } = res.json.mock.calls[0][0]; + expect(data).toHaveLength(1); + expect(data[0].title).toBe('Has Cost'); + }); + + it('should limit results to top 5 issues', async () => { + const mockIssues = Array.from({ length: 10 }, (_, i) => ({ + _id: `${i}`, + name: `Issue ${i}`, + openDate: new Date('2024-01-01'), + totalCost: (i + 1) * 100, + })); + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain(mockIssues)); + + await controller.getMostExpensiveIssues(req, res); + + const { data } = res.json.mock.calls[0][0]; + expect(data).toHaveLength(5); + // highest costs first + expect(data[0].totalCost).toBe(1000); + expect(data[4].totalCost).toBe(600); + }); + + it('should return empty data array when no issues found', async () => { + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + expect(res.json).toHaveBeenCalledWith({ data: [] }); + }); + + it('should return 500 error when database error occurs', async () => { + const error = new Error('Database error'); + mockInjuryIssue.find.mockReturnValue({ + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockRejectedValue(error), + }); + + await controller.getMostExpensiveIssues(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ message: 'Error fetching most expensive issues' }); + }); + }); +}); \ No newline at end of file From 2bad1d0b5a88eccbda54ea37d5241fac7c5ad034 Mon Sep 17 00:00:00 2001 From: Gayatri Sawant Date: Fri, 28 Aug 2026 01:25:45 -0400 Subject: [PATCH 7/7] Add test coverage for injury issue CRUD functions (bmPostInjuryIssue, bmGetInjuryIssue, bmDeleteInjuryIssue, bmRenameInjuryIssue, bmCopyInjuryIssue) to close SonarCloud coverage gap --- .../__tests__/bmIssueController.test.js | 222 +++++++++++++++++- 1 file changed, 219 insertions(+), 3 deletions(-) diff --git a/src/controllers/bmdashboard/__tests__/bmIssueController.test.js b/src/controllers/bmdashboard/__tests__/bmIssueController.test.js index ced8f2efa9..1545ff9d25 100644 --- a/src/controllers/bmdashboard/__tests__/bmIssueController.test.js +++ b/src/controllers/bmdashboard/__tests__/bmIssueController.test.js @@ -30,10 +30,13 @@ const mockBuildingIssue = { findByIdAndUpdate: jest.fn(), findByIdAndDelete: jest.fn(), }; - -// Mocking the injuryIssue Model (used by getMostExpensiveIssues) +// Mocking the injuryIssue Model (used by getMostExpensiveIssues and injury issue CRUD) const mockInjuryIssue = { find: jest.fn(), + findById: jest.fn(), + findByIdAndUpdate: jest.fn(), + findByIdAndDelete: jest.fn(), + create: jest.fn(), }; // Helper: builds the chained find mock used by getMostExpensiveIssues @@ -1056,6 +1059,219 @@ describe('Building Issue Controller', () => { expect(res.status).toHaveBeenCalledWith(500); expect(res.json).toHaveBeenCalledWith({ message: 'Error fetching most expensive issues' }); + }); +}); + + // ==================== bmPostInjuryIssue Tests ==================== + describe('bmPostInjuryIssue', () => { + it('should create a new injury issue successfully', async () => { + const mockIssue = { + _id: TEST_ISSUE_ID, + projectId: TEST_ISSUE_ID, + name: 'Injury Issue', + openDate: new Date('2024-01-01'), + category: 'Safety', + assignedTo: TEST_ISSUE_ID, + totalCost: 500, + }; + req.body = { ...mockIssue }; + mockInjuryIssue.create.mockResolvedValue(mockIssue); + + await controller.bmPostInjuryIssue(req, res); + + expect(mockInjuryIssue.create).toHaveBeenCalledWith( + expect.objectContaining({ name: 'Injury Issue', totalCost: 500 }), + ); + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith(mockIssue); + }); + + it('should return 400 when creation fails', async () => { + req.body = { name: 'Bad Issue' }; + mockInjuryIssue.create.mockRejectedValue(new Error('Validation failed')); + + await controller.bmPostInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: 'Validation failed' }); + }); + }); + + // ==================== bmGetInjuryIssue Tests ==================== + describe('bmGetInjuryIssue', () => { + it('should fetch all injury issues successfully', async () => { + const mockIssues = [{ _id: TEST_ISSUE_ID, name: 'Injury 1' }]; + mockInjuryIssue.find.mockReturnValue({ + populate: jest.fn().mockResolvedValue(mockIssues), + }); + + await controller.bmGetInjuryIssue(req, res); + + expect(mockInjuryIssue.find).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith(mockIssues); + }); + + it('should return 500 when database error occurs', async () => { + mockInjuryIssue.find.mockReturnValue({ + populate: jest.fn().mockRejectedValue(new Error('Database error')), + }); + + await controller.bmGetInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: 'Database error' }); + }); + }); + + // ==================== bmDeleteInjuryIssue Tests ==================== + describe('bmDeleteInjuryIssue', () => { + it('should delete an injury issue successfully', async () => { + req.params.id = TEST_ISSUE_ID; + const mockDeleted = { _id: TEST_ISSUE_ID }; + mockInjuryIssue.findByIdAndDelete.mockResolvedValue(mockDeleted); + + await controller.bmDeleteInjuryIssue(req, res); + + expect(mockInjuryIssue.findByIdAndDelete).toHaveBeenCalledWith(TEST_ISSUE_ID); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + message: 'Deleted successfully', + deleted: mockDeleted, + }); + }); + + it('should return 404 when issue not found', async () => { + req.params.id = TEST_ISSUE_ID; + mockInjuryIssue.findByIdAndDelete.mockResolvedValue(null); + + await controller.bmDeleteInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ message: 'Issue not found' }); + }); + + it('should return 500 when database error occurs', async () => { + req.params.id = TEST_ISSUE_ID; + mockInjuryIssue.findByIdAndDelete.mockRejectedValue(new Error('Database error')); + + await controller.bmDeleteInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: 'Database error' }); }); }); -}); \ No newline at end of file + + // ==================== bmRenameInjuryIssue Tests ==================== + describe('bmRenameInjuryIssue', () => { + it('should rename an injury issue successfully', async () => { + req.params.id = TEST_ISSUE_ID; + req.body = { newName: 'Renamed Issue' }; + const mockUpdated = { _id: TEST_ISSUE_ID, name: 'Renamed Issue' }; + mockInjuryIssue.findByIdAndUpdate.mockResolvedValue(mockUpdated); + + await controller.bmRenameInjuryIssue(req, res); + + expect(mockInjuryIssue.findByIdAndUpdate).toHaveBeenCalledWith( + TEST_ISSUE_ID, + { name: 'Renamed Issue' }, + { new: true, runValidators: true }, + ); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + message: 'Renamed successfully', + updated: mockUpdated, + }); + }); + + it('should return 400 when newName is not provided', async () => { + req.params.id = TEST_ISSUE_ID; + req.body = {}; + + await controller.bmRenameInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ message: 'newName is required' }); + expect(mockInjuryIssue.findByIdAndUpdate).not.toHaveBeenCalled(); + }); + + it('should return 404 when issue not found', async () => { + req.params.id = TEST_ISSUE_ID; + req.body = { newName: 'New Name' }; + mockInjuryIssue.findByIdAndUpdate.mockResolvedValue(null); + + await controller.bmRenameInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ message: 'Issue not found' }); + }); + + it('should return 500 when database error occurs', async () => { + req.params.id = TEST_ISSUE_ID; + req.body = { newName: 'New Name' }; + mockInjuryIssue.findByIdAndUpdate.mockRejectedValue(new Error('Database error')); + + await controller.bmRenameInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: 'Database error' }); + }); + }); + + // ==================== bmCopyInjuryIssue Tests ==================== + describe('bmCopyInjuryIssue', () => { + it('should copy an injury issue successfully', async () => { + req.params.id = TEST_ISSUE_ID; + const mockOriginal = { + _id: TEST_ISSUE_ID, + projectId: 'project1', + name: 'Original Issue', + category: 'Safety', + assignedTo: 'user1', + totalCost: 500, + }; + const mockCopy = { _id: '507f1f77bcf86cd799439099', name: 'Original Issue (Copy)' }; + mockInjuryIssue.findById.mockReturnValue({ + lean: jest.fn().mockResolvedValue(mockOriginal), + }); + mockInjuryIssue.create.mockResolvedValue(mockCopy); + + await controller.bmCopyInjuryIssue(req, res); + + expect(mockInjuryIssue.findById).toHaveBeenCalledWith(TEST_ISSUE_ID); + expect(mockInjuryIssue.create).toHaveBeenCalledWith( + expect.objectContaining({ name: 'Original Issue (Copy)', totalCost: 500 }), + ); + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith({ + message: 'Copied successfully', + copy: mockCopy, + }); + }); + + it('should return 404 when original issue not found', async () => { + req.params.id = TEST_ISSUE_ID; + mockInjuryIssue.findById.mockReturnValue({ + lean: jest.fn().mockResolvedValue(null), + }); + + await controller.bmCopyInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ message: 'Issue not found' }); + expect(mockInjuryIssue.create).not.toHaveBeenCalled(); + }); + + it('should return 500 when database error occurs', async () => { + req.params.id = TEST_ISSUE_ID; + mockInjuryIssue.findById.mockReturnValue({ + lean: jest.fn().mockRejectedValue(new Error('Database error')), + }); + + await controller.bmCopyInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: 'Database error' }); + }); + }); +});