Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0944447
fix: show all issues from selected projects without limit
vamsidharpanithi Jan 1, 2026
ea15b3a
feat: added values to support reordering warning trackers, and fixed …
AnthonyWeathers Apr 14, 2026
96e753a
refactor: fixed sonar listed issues
AnthonyWeathers Apr 24, 2026
9be97a2
chore: removed unneeded comment
AnthonyWeathers Apr 25, 2026
f84bba4
chore: merged development into branch
AnthonyWeathers May 4, 2026
d6bacc2
chore: merged development for new changes
AnthonyWeathers May 7, 2026
a834f32
refactor: Moved a bit of code around, and created a spec file with so…
AnthonyWeathers May 11, 2026
3df78df
chore: merged development for latest changes
AnthonyWeathers May 11, 2026
4d6944d
refactor: made changes to address issues pointed out by sonar
AnthonyWeathers May 11, 2026
b7d79df
refactor: added more test cases to raise coverage of currentWarningsC…
AnthonyWeathers May 19, 2026
43de691
chore: merged development into branch
AnthonyWeathers May 19, 2026
f5ef12f
chore: merged development for latest changes
AnthonyWeathers May 24, 2026
e5a774a
refactor: adjusted code to update the warnings for an individual when…
AnthonyWeathers May 26, 2026
3492220
refactor: removed leftover commented code and change a let variable t…
AnthonyWeathers May 28, 2026
35c670b
feat(bm): mount expenditure API routes
Aditya-gam Feb 21, 2026
1ab5fff
fix(bm): harden expenditure controller
Aditya-gam Feb 21, 2026
6384992
test(bm): add unit tests for expenditure controller
Aditya-gam Feb 21, 2026
0fce399
test(bm): add unit tests for updateEquipmentById
Aditya-gam Feb 24, 2026
a705ff3
test(summary-dashboard): add unit tests for labor hours distribution …
Aditya-gam Feb 24, 2026
93725c5
refactor: refactored order of routers to make them accessible
Aditya-gam Feb 25, 2026
b35ce2f
refactor(tests): consolidate labor hours distribution validation test…
Aditya-gam Feb 26, 2026
73f67de
fix: remove duplicate bmExpenditureRouter declaration
rithika-paii Jun 12, 2026
11b0200
resolve: merge conflicts with development branch
Jun 13, 2026
cf68852
Merge pull request #1982 from OneCommunityGlobal/vamsidhar-fix/issue-…
one-community Jun 13, 2026
5636ce7
Merge pull request #2073 from OneCommunityGlobal/Aditya-feat/Add-Side…
one-community Jun 13, 2026
5fb046b
chore(deps): bump joi from 18.0.2 to 18.2.1 (#2244)
dependabot[bot] Jun 14, 2026
df2b922
chore(deps-dev): bump tmp from 0.2.5 to 0.2.7 (#2236)
dependabot[bot] Jun 14, 2026
9d4fc8d
chore: merged development for changes and resolved merge conflict
AnthonyWeathers Jun 15, 2026
e8d10be
Merge pull request #2167 from OneCommunityGlobal/Anthony/feature-reor…
one-community Jun 16, 2026
078e9a7
fix(resendBS): Fixed Manual Resend Blue Square Emails
DiyaWadhwani Jun 17, 2026
57022ab
Merge pull request #2250 from OneCommunityGlobal/Diya_Fix_ResendManua…
one-community Jun 17, 2026
82b1180
fix(userProfile): update email validation in controller
Mahitha-pasupuleti Jun 17, 2026
1118ea3
fix(multiple): Fixed Weekly Summary Cron + BS History
DiyaWadhwani Jun 18, 2026
ca5d528
Merge pull request #2252 from OneCommunityGlobal/Diya_Fix_WeeklySumma…
one-community Jun 19, 2026
916578a
Merge pull request #2251 from OneCommunityGlobal/feature/volunteer-fi…
one-community Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,789 changes: 1,777 additions & 12 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
"geoip-lite": "^1.4.10",
"googleapis": "^100.0.0",
"isomorphic-fetch": "^3.0.0",
"joi": "^18.0.1",
"joi": "^18.2.1",
"json2csv": "^6.0.0-alpha.2",
"jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21",
Expand Down
1 change: 1 addition & 0 deletions src/controllers/activityController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const Activity = require('../models/activity');
const LessonPlan = require('../models/lessonPlan');
const Subject = require('../models/subject');
const Atom = require('../models/atom');

const activityController = function () {
// Get all activities
const getActivities = async (req, res) => {
Expand Down
145 changes: 145 additions & 0 deletions src/controllers/bmdashboard/__tests__/bmEquipmentController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('bmEquipmentController', () => {
findOneAndUpdate: jest.fn(),
create: jest.fn(),
findByIdAndUpdate: jest.fn(),
updateOne: jest.fn(),
};

// Initialize controller with mock model
Expand Down Expand Up @@ -130,4 +131,148 @@ describe('bmEquipmentController', () => {
expect(mockRes.status).toHaveBeenCalledWith(201);
});
});

describe('updateEquipmentById', () => {
const validObjectId = '507f1f77bcf86cd799439011';

it('should return 400 for invalid equipment ID', async () => {
mockReq.params.equipmentId = 'invalid-id';
mockReq.body = { purchaseStatus: 'Purchased' };

await controller.updateEquipmentById(mockReq, mockRes);

expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.send).toHaveBeenCalledWith({ message: 'Invalid equipment ID.' });
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
});

it('should return 400 for invalid project ID when provided', async () => {
mockReq.params.equipmentId = validObjectId;
mockReq.body = { projectId: 'not-valid', purchaseStatus: 'Purchased' };

await controller.updateEquipmentById(mockReq, mockRes);

expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.send).toHaveBeenCalledWith({ message: 'Invalid project ID.' });
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
});

it('should return 400 for invalid purchaseStatus enum', async () => {
mockReq.params.equipmentId = validObjectId;
mockReq.body = { purchaseStatus: 'InvalidStatus' };

await controller.updateEquipmentById(mockReq, mockRes);

expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.send).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Invalid purchaseStatus'),
}),
);
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
});

it('should return 400 for invalid currentUsage enum', async () => {
mockReq.params.equipmentId = validObjectId;
mockReq.body = { currentUsage: 'Broken' };

await controller.updateEquipmentById(mockReq, mockRes);

expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.send).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Invalid currentUsage'),
}),
);
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
});

it('should return 400 for invalid condition enum', async () => {
mockReq.params.equipmentId = validObjectId;
mockReq.body = { condition: 'Unknown' };

await controller.updateEquipmentById(mockReq, mockRes);

expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.send).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Invalid condition'),
}),
);
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
});

it('should return 400 when no valid fields provided to update', async () => {
mockReq.params.equipmentId = validObjectId;
mockReq.body = { unknownField: 'value' };

await controller.updateEquipmentById(mockReq, mockRes);

expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.send).toHaveBeenCalledWith({
message: 'No valid fields provided to update.',
});
expect(mockBuildingEquipment.updateOne).not.toHaveBeenCalled();
});

it('should return 200 with updated equipment on success', async () => {
const updatedEquipment = {
_id: validObjectId,
purchaseStatus: 'Purchased',
condition: 'Good',
};
mockReq.params.equipmentId = validObjectId;
mockReq.body = { purchaseStatus: 'Purchased', condition: 'Good' };

mockBuildingEquipment.updateOne.mockResolvedValue({});
const mockPopulate = jest.fn().mockReturnThis();
const mockExec = jest.fn().mockResolvedValue(updatedEquipment);
mockBuildingEquipment.findById.mockReturnValue({
populate: mockPopulate,
exec: mockExec,
});

await controller.updateEquipmentById(mockReq, mockRes);

expect(mockBuildingEquipment.updateOne).toHaveBeenCalledWith(
{ _id: validObjectId },
{ $set: expect.objectContaining({ purchaseStatus: 'Purchased', condition: 'Good' }) },
);
expect(mockBuildingEquipment.findById).toHaveBeenCalledWith(validObjectId);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.send).toHaveBeenCalledWith(updatedEquipment);
});

it('should return 404 when equipment not found after update', async () => {
mockReq.params.equipmentId = validObjectId;
mockReq.body = { condition: 'Good' };

mockBuildingEquipment.updateOne.mockResolvedValue({});
const mockPopulate = jest.fn().mockReturnThis();
const mockExec = jest.fn().mockResolvedValue(null);
mockBuildingEquipment.findById.mockReturnValue({
populate: mockPopulate,
exec: mockExec,
});

await controller.updateEquipmentById(mockReq, mockRes);

expect(mockRes.status).toHaveBeenCalledWith(404);
expect(mockRes.send).toHaveBeenCalledWith({ message: 'Equipment not found.' });
});

it('should return 500 on updateOne error', async () => {
mockReq.params.equipmentId = validObjectId;
mockReq.body = { condition: 'Good' };

mockBuildingEquipment.updateOne.mockRejectedValue(new Error('DB error'));

await controller.updateEquipmentById(mockReq, mockRes);

expect(mockRes.status).toHaveBeenCalledWith(500);
expect(mockRes.send).toHaveBeenCalledWith(
expect.objectContaining({ message: expect.any(String) }),
);
});
});
});
148 changes: 148 additions & 0 deletions src/controllers/bmdashboard/__tests__/expenditureController.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
jest.mock('../../../models/bmdashboard/expenditure', () => ({
distinct: jest.fn(),
aggregate: jest.fn(),
}));

jest.mock('../../../startup/logger', () => ({
logException: jest.fn(),
logInfo: jest.fn(),
}));

const Expenditure = require('../../../models/bmdashboard/expenditure');
const logger = require('../../../startup/logger');
const { getProjectExpensesPie, getProjectIdsWithExpenditure } = require('../expenditureController');

const VALID_ID = '507f1f77bcf86cd799439011';
const INVALID_ID = 'not-a-valid-id';

const makeRes = () => {
const res = {};
res.status = jest.fn().mockReturnThis();
res.json = jest.fn().mockReturnThis();
return res;
};

describe('expenditureController', () => {
beforeEach(() => {
jest.clearAllMocks();
});

// ---------------------------------------------------------------------------
describe('getProjectIdsWithExpenditure', () => {
it('returns 200 with the array of project IDs on success', async () => {
const ids = [VALID_ID, '507f1f77bcf86cd799439012'];
Expenditure.distinct.mockResolvedValue(ids);

const res = makeRes();
await getProjectIdsWithExpenditure({}, res);

expect(Expenditure.distinct).toHaveBeenCalledWith('projectId');
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(ids);
});

it('returns 200 with an empty array when no expenditure records exist', async () => {
Expenditure.distinct.mockResolvedValue([]);

const res = makeRes();
await getProjectIdsWithExpenditure({}, res);

expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith([]);
});

it('returns 500 and calls logger on DB error', async () => {
const error = new Error('DB failure');
Expenditure.distinct.mockRejectedValue(error);

const res = makeRes();
await getProjectIdsWithExpenditure({}, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({ message: 'Failed to retrieve project IDs' });
expect(logger.logException).toHaveBeenCalledWith(
error,
'expenditureController.getProjectIdsWithExpenditure',
);
});
});

// ---------------------------------------------------------------------------
describe('getProjectExpensesPie', () => {
it('returns 400 for an invalid projectId without calling aggregate', async () => {
const req = { params: { projectId: INVALID_ID } };
const res = makeRes();

await getProjectExpensesPie(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ message: 'Invalid project ID' });
expect(Expenditure.aggregate).not.toHaveBeenCalled();
});

it('returns 400 for a short non-hex string without calling aggregate', async () => {
const req = { params: { projectId: '12345' } };
const res = makeRes();

await getProjectExpensesPie(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(Expenditure.aggregate).not.toHaveBeenCalled();
});

it('returns 200 with { actual, planned } shape on success', async () => {
Expenditure.aggregate.mockResolvedValue([
{ _id: { type: 'actual', category: 'Labor' }, amount: 1000 },
{ _id: { type: 'actual', category: 'Materials' }, amount: 500 },
{ _id: { type: 'planned', category: 'Equipment' }, amount: 2000 },
]);

const req = { params: { projectId: VALID_ID } };
const res = makeRes();

await getProjectExpensesPie(req, res);

expect(Expenditure.aggregate).toHaveBeenCalledTimes(1);
expect(res.json).toHaveBeenCalledWith({
actual: [
{ category: 'Labor', amount: 1000 },
{ category: 'Materials', amount: 500 },
],
planned: [{ category: 'Equipment', amount: 2000 }],
});
// status should NOT be explicitly set to 200 (res.json implies 200)
expect(res.status).not.toHaveBeenCalled();
});

it('returns { actual: [], planned: [] } when no matching records exist', async () => {
Expenditure.aggregate.mockResolvedValue([]);

const req = { params: { projectId: VALID_ID } };
const res = makeRes();

await getProjectExpensesPie(req, res);

expect(res.json).toHaveBeenCalledWith({ actual: [], planned: [] });
});

it('returns 500 and calls logger with projectId on DB error', async () => {
const error = new Error('Aggregate failed');
Expenditure.aggregate.mockRejectedValue(error);

const req = { params: { projectId: VALID_ID } };
const res = makeRes();

await getProjectExpensesPie(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
message: 'Server error retrieving expenses pie data.',
});
expect(logger.logException).toHaveBeenCalledWith(
error,
'expenditureController.getProjectExpensesPie',
{ projectId: VALID_ID },
);
});
});
});
37 changes: 28 additions & 9 deletions src/controllers/bmdashboard/bmIssueController.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,18 +273,37 @@ const bmIssueController = function (BuildingIssue, injuryIssue) {
query.projectId = { $in: filteredProjectIds };
}

const issues = await BuildingIssue.find(query)
.select('issueTitle issueDate projectId')
.populate({
path: 'projectId',
select: 'projectName name',
})
let issues = await BuildingIssue.find(query)
.select('issueTitle issueDate _id')
.populate('projectId')
.lean();

const grouped = buildGroupedIssues(issues);
const response = buildLongestOpenResponse(grouped);
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`,
);

res.json(response);
res.json(sortedIssues);
} catch (error) {
res.status(500).json({ message: 'Error fetching longest open issues' });
}
Expand Down
Loading
Loading