Skip to content

Commit 9d4fc8d

Browse files
chore: merged development for changes and resolved merge conflict
2 parents 3492220 + df2b922 commit 9d4fc8d

22 files changed

Lines changed: 1161 additions & 109 deletions

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
"geoip-lite": "^1.4.10",
105105
"googleapis": "^100.0.0",
106106
"isomorphic-fetch": "^3.0.0",
107-
"joi": "^18.0.1",
107+
"joi": "^18.2.1",
108108
"json2csv": "^6.0.0-alpha.2",
109109
"jsonwebtoken": "^9.0.0",
110110
"lodash": "^4.17.21",

src/controllers/bmdashboard/__tests__/bmEquipmentController.test.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe('bmEquipmentController', () => {
1515
findOneAndUpdate: jest.fn(),
1616
create: jest.fn(),
1717
findByIdAndUpdate: jest.fn(),
18+
updateOne: jest.fn(),
1819
};
1920

2021
// Initialize controller with mock model
@@ -130,4 +131,148 @@ describe('bmEquipmentController', () => {
130131
expect(mockRes.status).toHaveBeenCalledWith(201);
131132
});
132133
});
134+
135+
describe('updateEquipmentById', () => {
136+
const validObjectId = '507f1f77bcf86cd799439011';
137+
138+
it('should return 400 for invalid equipment ID', async () => {
139+
mockReq.params.equipmentId = 'invalid-id';
140+
mockReq.body = { purchaseStatus: 'Purchased' };
141+
142+
await controller.updateEquipmentById(mockReq, mockRes);
143+
144+
expect(mockRes.status).toHaveBeenCalledWith(400);
145+
expect(mockRes.send).toHaveBeenCalledWith({ message: 'Invalid equipment ID.' });
146+
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
147+
});
148+
149+
it('should return 400 for invalid project ID when provided', async () => {
150+
mockReq.params.equipmentId = validObjectId;
151+
mockReq.body = { projectId: 'not-valid', purchaseStatus: 'Purchased' };
152+
153+
await controller.updateEquipmentById(mockReq, mockRes);
154+
155+
expect(mockRes.status).toHaveBeenCalledWith(400);
156+
expect(mockRes.send).toHaveBeenCalledWith({ message: 'Invalid project ID.' });
157+
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
158+
});
159+
160+
it('should return 400 for invalid purchaseStatus enum', async () => {
161+
mockReq.params.equipmentId = validObjectId;
162+
mockReq.body = { purchaseStatus: 'InvalidStatus' };
163+
164+
await controller.updateEquipmentById(mockReq, mockRes);
165+
166+
expect(mockRes.status).toHaveBeenCalledWith(400);
167+
expect(mockRes.send).toHaveBeenCalledWith(
168+
expect.objectContaining({
169+
message: expect.stringContaining('Invalid purchaseStatus'),
170+
}),
171+
);
172+
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
173+
});
174+
175+
it('should return 400 for invalid currentUsage enum', async () => {
176+
mockReq.params.equipmentId = validObjectId;
177+
mockReq.body = { currentUsage: 'Broken' };
178+
179+
await controller.updateEquipmentById(mockReq, mockRes);
180+
181+
expect(mockRes.status).toHaveBeenCalledWith(400);
182+
expect(mockRes.send).toHaveBeenCalledWith(
183+
expect.objectContaining({
184+
message: expect.stringContaining('Invalid currentUsage'),
185+
}),
186+
);
187+
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
188+
});
189+
190+
it('should return 400 for invalid condition enum', async () => {
191+
mockReq.params.equipmentId = validObjectId;
192+
mockReq.body = { condition: 'Unknown' };
193+
194+
await controller.updateEquipmentById(mockReq, mockRes);
195+
196+
expect(mockRes.status).toHaveBeenCalledWith(400);
197+
expect(mockRes.send).toHaveBeenCalledWith(
198+
expect.objectContaining({
199+
message: expect.stringContaining('Invalid condition'),
200+
}),
201+
);
202+
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
203+
});
204+
205+
it('should return 400 when no valid fields provided to update', async () => {
206+
mockReq.params.equipmentId = validObjectId;
207+
mockReq.body = { unknownField: 'value' };
208+
209+
await controller.updateEquipmentById(mockReq, mockRes);
210+
211+
expect(mockRes.status).toHaveBeenCalledWith(400);
212+
expect(mockRes.send).toHaveBeenCalledWith({
213+
message: 'No valid fields provided to update.',
214+
});
215+
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
216+
});
217+
218+
it('should return 200 with updated equipment on success', async () => {
219+
const updatedEquipment = {
220+
_id: validObjectId,
221+
purchaseStatus: 'Purchased',
222+
condition: 'Good',
223+
};
224+
mockReq.params.equipmentId = validObjectId;
225+
mockReq.body = { purchaseStatus: 'Purchased', condition: 'Good' };
226+
227+
mockBuildingEquipment.updateOne.mockResolvedValue({});
228+
const mockPopulate = jest.fn().mockReturnThis();
229+
const mockExec = jest.fn().mockResolvedValue(updatedEquipment);
230+
mockBuildingEquipment.findById.mockReturnValue({
231+
populate: mockPopulate,
232+
exec: mockExec,
233+
});
234+
235+
await controller.updateEquipmentById(mockReq, mockRes);
236+
237+
expect(mockBuildingEquipment.updateOne).toHaveBeenCalledWith(
238+
{ _id: validObjectId },
239+
{ $set: expect.objectContaining({ purchaseStatus: 'Purchased', condition: 'Good' }) },
240+
);
241+
expect(mockBuildingEquipment.findById).toHaveBeenCalledWith(validObjectId);
242+
expect(mockRes.status).toHaveBeenCalledWith(200);
243+
expect(mockRes.send).toHaveBeenCalledWith(updatedEquipment);
244+
});
245+
246+
it('should return 404 when equipment not found after update', async () => {
247+
mockReq.params.equipmentId = validObjectId;
248+
mockReq.body = { condition: 'Good' };
249+
250+
mockBuildingEquipment.updateOne.mockResolvedValue({});
251+
const mockPopulate = jest.fn().mockReturnThis();
252+
const mockExec = jest.fn().mockResolvedValue(null);
253+
mockBuildingEquipment.findById.mockReturnValue({
254+
populate: mockPopulate,
255+
exec: mockExec,
256+
});
257+
258+
await controller.updateEquipmentById(mockReq, mockRes);
259+
260+
expect(mockRes.status).toHaveBeenCalledWith(404);
261+
expect(mockRes.send).toHaveBeenCalledWith({ message: 'Equipment not found.' });
262+
});
263+
264+
it('should return 500 on updateOne error', async () => {
265+
mockReq.params.equipmentId = validObjectId;
266+
mockReq.body = { condition: 'Good' };
267+
268+
mockBuildingEquipment.updateOne.mockRejectedValue(new Error('DB error'));
269+
270+
await controller.updateEquipmentById(mockReq, mockRes);
271+
272+
expect(mockRes.status).toHaveBeenCalledWith(500);
273+
expect(mockRes.send).toHaveBeenCalledWith(
274+
expect.objectContaining({ message: expect.any(String) }),
275+
);
276+
});
277+
});
133278
});
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
jest.mock('../../../models/bmdashboard/expenditure', () => ({
2+
distinct: jest.fn(),
3+
aggregate: jest.fn(),
4+
}));
5+
6+
jest.mock('../../../startup/logger', () => ({
7+
logException: jest.fn(),
8+
logInfo: jest.fn(),
9+
}));
10+
11+
const Expenditure = require('../../../models/bmdashboard/expenditure');
12+
const logger = require('../../../startup/logger');
13+
const { getProjectExpensesPie, getProjectIdsWithExpenditure } = require('../expenditureController');
14+
15+
const VALID_ID = '507f1f77bcf86cd799439011';
16+
const INVALID_ID = 'not-a-valid-id';
17+
18+
const makeRes = () => {
19+
const res = {};
20+
res.status = jest.fn().mockReturnThis();
21+
res.json = jest.fn().mockReturnThis();
22+
return res;
23+
};
24+
25+
describe('expenditureController', () => {
26+
beforeEach(() => {
27+
jest.clearAllMocks();
28+
});
29+
30+
// ---------------------------------------------------------------------------
31+
describe('getProjectIdsWithExpenditure', () => {
32+
it('returns 200 with the array of project IDs on success', async () => {
33+
const ids = [VALID_ID, '507f1f77bcf86cd799439012'];
34+
Expenditure.distinct.mockResolvedValue(ids);
35+
36+
const res = makeRes();
37+
await getProjectIdsWithExpenditure({}, res);
38+
39+
expect(Expenditure.distinct).toHaveBeenCalledWith('projectId');
40+
expect(res.status).toHaveBeenCalledWith(200);
41+
expect(res.json).toHaveBeenCalledWith(ids);
42+
});
43+
44+
it('returns 200 with an empty array when no expenditure records exist', async () => {
45+
Expenditure.distinct.mockResolvedValue([]);
46+
47+
const res = makeRes();
48+
await getProjectIdsWithExpenditure({}, res);
49+
50+
expect(res.status).toHaveBeenCalledWith(200);
51+
expect(res.json).toHaveBeenCalledWith([]);
52+
});
53+
54+
it('returns 500 and calls logger on DB error', async () => {
55+
const error = new Error('DB failure');
56+
Expenditure.distinct.mockRejectedValue(error);
57+
58+
const res = makeRes();
59+
await getProjectIdsWithExpenditure({}, res);
60+
61+
expect(res.status).toHaveBeenCalledWith(500);
62+
expect(res.json).toHaveBeenCalledWith({ message: 'Failed to retrieve project IDs' });
63+
expect(logger.logException).toHaveBeenCalledWith(
64+
error,
65+
'expenditureController.getProjectIdsWithExpenditure',
66+
);
67+
});
68+
});
69+
70+
// ---------------------------------------------------------------------------
71+
describe('getProjectExpensesPie', () => {
72+
it('returns 400 for an invalid projectId without calling aggregate', async () => {
73+
const req = { params: { projectId: INVALID_ID } };
74+
const res = makeRes();
75+
76+
await getProjectExpensesPie(req, res);
77+
78+
expect(res.status).toHaveBeenCalledWith(400);
79+
expect(res.json).toHaveBeenCalledWith({ message: 'Invalid project ID' });
80+
expect(Expenditure.aggregate).not.toHaveBeenCalled();
81+
});
82+
83+
it('returns 400 for a short non-hex string without calling aggregate', async () => {
84+
const req = { params: { projectId: '12345' } };
85+
const res = makeRes();
86+
87+
await getProjectExpensesPie(req, res);
88+
89+
expect(res.status).toHaveBeenCalledWith(400);
90+
expect(Expenditure.aggregate).not.toHaveBeenCalled();
91+
});
92+
93+
it('returns 200 with { actual, planned } shape on success', async () => {
94+
Expenditure.aggregate.mockResolvedValue([
95+
{ _id: { type: 'actual', category: 'Labor' }, amount: 1000 },
96+
{ _id: { type: 'actual', category: 'Materials' }, amount: 500 },
97+
{ _id: { type: 'planned', category: 'Equipment' }, amount: 2000 },
98+
]);
99+
100+
const req = { params: { projectId: VALID_ID } };
101+
const res = makeRes();
102+
103+
await getProjectExpensesPie(req, res);
104+
105+
expect(Expenditure.aggregate).toHaveBeenCalledTimes(1);
106+
expect(res.json).toHaveBeenCalledWith({
107+
actual: [
108+
{ category: 'Labor', amount: 1000 },
109+
{ category: 'Materials', amount: 500 },
110+
],
111+
planned: [{ category: 'Equipment', amount: 2000 }],
112+
});
113+
// status should NOT be explicitly set to 200 (res.json implies 200)
114+
expect(res.status).not.toHaveBeenCalled();
115+
});
116+
117+
it('returns { actual: [], planned: [] } when no matching records exist', async () => {
118+
Expenditure.aggregate.mockResolvedValue([]);
119+
120+
const req = { params: { projectId: VALID_ID } };
121+
const res = makeRes();
122+
123+
await getProjectExpensesPie(req, res);
124+
125+
expect(res.json).toHaveBeenCalledWith({ actual: [], planned: [] });
126+
});
127+
128+
it('returns 500 and calls logger with projectId on DB error', async () => {
129+
const error = new Error('Aggregate failed');
130+
Expenditure.aggregate.mockRejectedValue(error);
131+
132+
const req = { params: { projectId: VALID_ID } };
133+
const res = makeRes();
134+
135+
await getProjectExpensesPie(req, res);
136+
137+
expect(res.status).toHaveBeenCalledWith(500);
138+
expect(res.json).toHaveBeenCalledWith({
139+
message: 'Server error retrieving expenses pie data.',
140+
});
141+
expect(logger.logException).toHaveBeenCalledWith(
142+
error,
143+
'expenditureController.getProjectExpensesPie',
144+
{ projectId: VALID_ID },
145+
);
146+
});
147+
});
148+
});

src/controllers/bmdashboard/bmIssueController.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -273,18 +273,37 @@ const bmIssueController = function (BuildingIssue, injuryIssue) {
273273
query.projectId = { $in: filteredProjectIds };
274274
}
275275

276-
const issues = await BuildingIssue.find(query)
277-
.select('issueTitle issueDate projectId')
278-
.populate({
279-
path: 'projectId',
280-
select: 'projectName name',
281-
})
276+
let issues = await BuildingIssue.find(query)
277+
.select('issueTitle issueDate _id')
278+
.populate('projectId')
282279
.lean();
283280

284-
const grouped = buildGroupedIssues(issues);
285-
const response = buildLongestOpenResponse(grouped);
281+
issues = issues.map((issue) => {
282+
const durationInMonths = getDurationOpenMonths(issue.issueDate);
283+
return {
284+
issueName: issue.issueTitle && issue.issueTitle.length > 0 ? issue.issueTitle[0] : null,
285+
durationInMonths,
286+
issueId: issue._id.toString(),
287+
projectId: issue.projectId?._id?.toString() || issue.projectId?.toString(),
288+
projectName: issue.projectId?.name || null,
289+
};
290+
});
291+
292+
const sortedIssues = issues
293+
.sort((a, b) => b.durationInMonths - a.durationInMonths)
294+
.map(({ issueName, durationInMonths, issueId, projectId, projectName }) => ({
295+
issueName,
296+
durationOpen: durationInMonths,
297+
issueId,
298+
projectId,
299+
projectName,
300+
}));
301+
302+
console.log(
303+
`[getLongestOpenIssues] Total issues found: ${issues.length}, Returning: ${sortedIssues.length} issues`,
304+
);
286305

287-
res.json(response);
306+
res.json(sortedIssues);
288307
} catch (error) {
289308
res.status(500).json({ message: 'Error fetching longest open issues' });
290309
}

0 commit comments

Comments
 (0)