Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9c61a34
feat: add date parsing and normalization utilities for material cost …
Aditya-gam Dec 13, 2025
4becaa2
feat: add multi-select query parameter parser utility
Aditya-gam Dec 13, 2025
8b5f195
feat: add material aggregation helper utilities for cost correlation
Aditya-gam Dec 13, 2025
a9dce10
refactor: reduce parameter count in aggregation functions
Aditya-gam Dec 13, 2025
db7232e
feat: add response builder utility for material cost correlation
Aditya-gam Dec 13, 2025
f6c2cf6
feat: implement material cost correlation controller method
Aditya-gam Dec 13, 2025
e7e2e8a
refactor: extract magic numbers and replace console.error in bmMateri…
Aditya-gam Dec 13, 2025
2802aa4
feat: add route for material cost correlation endpoint
Aditya-gam Dec 13, 2025
6156b7d
refactor: use HTTP status constant in error handling
Aditya-gam Dec 13, 2025
58dd4bc
refactor: extract ObjectId conversion to eliminate duplication
Aditya-gam Dec 13, 2025
b2f8d20
test: move queryParamParser test to __tests__ directory
Aditya-gam Dec 13, 2025
e0acbc9
test: add comprehensive tests for materialCostCorrelationDateUtils
Aditya-gam Dec 13, 2025
d3a41fe
test: add comprehensive tests for materialCostCorrelationHelpers
Aditya-gam Dec 13, 2025
40b51d6
test: add comprehensive tests for bmGetMaterialCostCorrelation
Aditya-gam Dec 13, 2025
18495d2
test: add router tests for bmMaterialsRouter cost-correlation route
Aditya-gam Dec 13, 2025
b5525f6
feat: add name-based query parameters for material cost correlation
Aditya-gam Dec 13, 2025
c0599c4
refactor: improve cost correlation data handling and reduce complexity
Aditya-gam Dec 13, 2025
04db7ea
fix(bm): use correct BuildingMaterial model for cost correlation API
Aditya-gam Dec 18, 2025
ad922af
refactor(bm): remove debugging logs and unnecessary exception logging
Aditya-gam Dec 18, 2025
68fec92
test(bm): fix and update tests for material cost correlation
Aditya-gam Dec 18, 2025
5ca2720
fix: fixed the mock of the controller
Aditya-gam Feb 17, 2026
95ff1cc
refactor(helpers): DRY and lint fixes in material cost correlation he…
Aditya-gam Mar 3, 2026
8d6fcea
test(helpers): DRY material cost correlation test utilities
Aditya-gam Mar 3, 2026
f9b149b
fix(security): validate ObjectIds in queries; refactor BM materials c…
Aditya-gam Mar 3, 2026
bce9c3e
test(router): disable x-powered-by in BM materials router tests
Aditya-gam Mar 3, 2026
e0bb61f
test(controller): update BM materials tests for ObjectId-safe queries
Aditya-gam Mar 3, 2026
b8cf361
fix(security): validate material._id in bmPostMaterialUpdateRecord
Aditya-gam Mar 3, 2026
7fbe703
fix: remove duplicate bmGetMaterialStockOutRisk declaration
rithika-paii May 8, 2026
340a24a
fix: improve fallback labels for unresolved project/material type ref…
rithika-paii Jun 30, 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
726 changes: 708 additions & 18 deletions src/controllers/bmdashboard/__tests__/bmMaterialsController.test.js

Large diffs are not rendered by default.

748 changes: 488 additions & 260 deletions src/controllers/bmdashboard/bmMaterialsController.js

Large diffs are not rendered by default.

100 changes: 65 additions & 35 deletions src/controllers/bmdashboard/bmMaterialsController.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
const bmMaterialsController = require('./bmMaterialsController');

jest.mock('mongoose', () => ({
Types: {
ObjectId: jest.fn((id) => id),
},
}));
jest.mock('mongoose', () => {
const mockObjectId = jest.fn((id) => id);
mockObjectId.isValid = (id) => typeof id === 'string' && /^[a-fA-F0-9]{24}$/.test(id);
return {
Types: {
ObjectId: mockObjectId,
},
};
});

describe('bmMaterialsController', () => {
let BuildingMaterialMock;
Expand Down Expand Up @@ -137,33 +141,37 @@ describe('bmMaterialsController', () => {

await controller.bmPurchaseMaterials(req, res);

expect(BuildingMaterialMock.findOne).toHaveBeenCalledWith({
project: validProjectId,
itemType: validMaterialTypeId,
});
expect(BuildingMaterialMock.create).toHaveBeenCalledWith({
itemType: validMaterialTypeId,
project: validProjectId,
purchaseRecord: [
{
quantity: 100,
priority: 'High',
brandPref: 'Premium Brand',
requestedBy: validRequestorId,
},
],
stockBought: 100,
});
expect(BuildingMaterialMock.findOne).toHaveBeenCalledWith(
expect.objectContaining({
project: expect.anything(),
itemType: expect.anything(),
}),
);
expect(BuildingMaterialMock.create).toHaveBeenCalledWith(
expect.objectContaining({
purchaseRecord: [
{
quantity: 100,
priority: 'High',
brandPref: 'Premium Brand',
requestedBy: validRequestorId,
},
],
stockBought: 100,
}),
);
expect(res.status).toHaveBeenCalledWith(201);
expect(res.send).toHaveBeenCalled();
});
});

describe('bmPostMaterialUpdateRecord', () => {
const validMaterialId = '507f1f77bcf86cd799439011';

it('should update material stock with valid quantities', async () => {
const updateData = {
material: {
_id: 'material123',
_id: validMaterialId,
stockAvailable: 100,
stockUsed: 20,
stockWasted: 5,
Expand All @@ -184,7 +192,7 @@ describe('bmMaterialsController', () => {
await controller.bmPostMaterialUpdateRecord(req, res);

expect(BuildingMaterialMock.updateOne).toHaveBeenCalledWith(
{ _id: 'material123' },
expect.objectContaining({ _id: expect.anything() }),
{
$set: {
stockUsed: 30,
Expand All @@ -208,7 +216,7 @@ describe('bmMaterialsController', () => {
it('should handle percentage-based quantity calculations correctly', async () => {
const updateData = {
material: {
_id: 'material123',
_id: validMaterialId,
stockAvailable: 100,
stockUsed: 20,
stockWasted: 5,
Expand All @@ -229,7 +237,7 @@ describe('bmMaterialsController', () => {
await controller.bmPostMaterialUpdateRecord(req, res);

expect(BuildingMaterialMock.updateOne).toHaveBeenCalledWith(
{ _id: 'material123' },
expect.objectContaining({ _id: expect.anything() }),
{
$set: {
stockUsed: 45,
Expand Down Expand Up @@ -281,9 +289,11 @@ describe('bmMaterialsController', () => {
});

describe('bmupdatePurchaseStatus', () => {
const validPurchaseId = '507f1f77bcf86cd7994390bb';

it('should successfully update purchase status from Pending to Approved', async () => {
const updateData = {
purchaseId: 'purchase123',
purchaseId: validPurchaseId,
status: 'Approved',
quantity: 100,
};
Expand All @@ -292,24 +302,25 @@ describe('bmMaterialsController', () => {

const mockMaterial = {
_id: 'material123',
purchaseRecord: [{ _id: 'purchase123', status: 'Pending', quantity: 100 }],
purchaseRecord: [{ _id: validPurchaseId, status: 'Pending', quantity: 100 }],
};

BuildingMaterialMock.findOne.mockResolvedValue(mockMaterial);
BuildingMaterialMock.findOneAndUpdate.mockResolvedValue(mockMaterial);

await controller.bmupdatePurchaseStatus(req, res);

expect(BuildingMaterialMock.findOne).toHaveBeenCalledWith({
'purchaseRecord._id': 'purchase123',
});
// Controller does NOT call findOneAndUpdate; don't assert it
expect(BuildingMaterialMock.findOne).toHaveBeenCalledWith(
expect.objectContaining({
'purchaseRecord._id': expect.anything(),
}),
);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith('Purchase approved successfully');
});
it('should return 404 when purchase is not found', async () => {
const updateData = {
purchaseId: 'nonexistentPurchase',
purchaseId: '507f1f77bcf86cd799439099',
status: 'Approved',
quantity: 100,
};
Expand All @@ -324,9 +335,28 @@ describe('bmMaterialsController', () => {
expect(res.send).toHaveBeenCalledWith('Purchase not found');
});

it('should return 400 when purchaseId is invalid', async () => {
req.body = {
purchaseId: 'not-valid-objectid',
status: 'Approved',
quantity: 100,
};

await controller.bmupdatePurchaseStatus(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Invalid purchase ID format',
field: 'purchaseId',
}),
);
expect(BuildingMaterialMock.findOne).not.toHaveBeenCalled();
});

it('should return 400 when trying to update non-Pending purchase', async () => {
const updateData = {
purchaseId: 'purchase123',
purchaseId: validPurchaseId,
status: 'Approved',
quantity: 100,
};
Expand All @@ -337,7 +367,7 @@ describe('bmMaterialsController', () => {
_id: 'material123',
purchaseRecord: [
{
_id: 'purchase123',
_id: validPurchaseId,
status: 'Approved',
quantity: 100,
},
Expand Down
Empty file.
127 changes: 127 additions & 0 deletions src/routes/bmdashboard/__tests__/bmMaterialsRouter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const express = require('express');
// Mock the controller
const mockController = {
bmMaterialsList: jest.fn(),
bmPurchaseMaterials: jest.fn(),
bmPostMaterialUpdateRecord: jest.fn(),
bmPostMaterialUpdateBulk: jest.fn(),
bmupdatePurchaseStatus: jest.fn(),
bmGetMaterialCostCorrelation: jest.fn(),
bmGetMaterialStockOutRisk: jest.fn(),
bmGetMaterialSummaryByProject: jest.fn(),
};

jest.mock('../../../controllers/bmdashboard/bmMaterialsController', () =>
jest.fn(() => mockController),
);

const bmMaterialsRouter = require('../bmMaterialsRouter');

describe('bmMaterialsRouter', () => {
let mockBuildingMaterial;
let router;

beforeEach(() => {
jest.clearAllMocks();
mockBuildingMaterial = {};
router = bmMaterialsRouter(mockBuildingMaterial);
});

describe('Category 1: Route Registration', () => {
it('should register route at correct path: /materials/cost-correlation', () => {
// Verify router is an Express Router instance
expect(router).toBeDefined();
expect(typeof router.route).toBe('function');
});

it('should register GET method for cost-correlation route', () => {
// Create a test app to verify route registration
const app = express();
app.disable('x-powered-by');
app.use('/test', router);

// The route should be registered - we verify by checking the controller is called
// when the route is accessed (in integration test)
expect(mockController.bmGetMaterialCostCorrelation).toBeDefined();
});

it('should bind controller method correctly', () => {
// Verify controller method exists and is a function
expect(typeof mockController.bmGetMaterialCostCorrelation).toBe('function');
});

it('should make route accessible', () => {
// Verify router is returned and can be used
expect(router).toBeDefined();
expect(typeof router.route).toBe('function');
expect(typeof router.use).toBe('function');
});
});

describe('Category 2: Route Ordering', () => {
it('should register cost-correlation route before /materials/:projectId route', () => {
// Verify both routes exist by checking controller methods
expect(mockController.bmGetMaterialCostCorrelation).toBeDefined();
expect(mockController.bmGetMaterialSummaryByProject).toBeDefined();

// The route order is determined by the order in the router file
// Line 19: /materials/cost-correlation
// Line 21: /materials/:projectId
// This ensures cost-correlation matches before the parameterized route
});

it('should ensure specific route matches before parameterized route', () => {
// Create Express app to test route matching
const app = express();
app.disable('x-powered-by');
app.use('/api/bm', router);

// Mock request handlers
mockController.bmGetMaterialCostCorrelation.mockImplementation((req, res) => {
res.status(200).json({ route: 'cost-correlation' });
});

mockController.bmGetMaterialSummaryByProject.mockImplementation((req, res) => {
res.status(200).json({ route: 'project-summary', projectId: req.params.projectId });
});

// Both routes should be registered
expect(mockController.bmGetMaterialCostCorrelation).toBeDefined();
expect(mockController.bmGetMaterialSummaryByProject).toBeDefined();
});

it('should ensure cost-correlation is not matched as projectId parameter', () => {
// Verify that cost-correlation route exists separately from parameterized route
expect(mockController.bmGetMaterialCostCorrelation).toBeDefined();
expect(mockController.bmGetMaterialSummaryByProject).toBeDefined();

// The route registration order ensures cost-correlation is checked first
// This prevents 'cost-correlation' from being treated as a projectId
});
});

describe('Category 3: Router Structure', () => {
it('should return Express Router instance', () => {
expect(router).toBeDefined();
expect(typeof router.route).toBe('function');
expect(typeof router.use).toBe('function');
expect(typeof router.get).toBe('function');
});

it('should initialize controller with BuildingMaterial model', () => {
const bmMaterialsController = require('../../../controllers/bmdashboard/bmMaterialsController');
expect(bmMaterialsController).toHaveBeenCalledWith(mockBuildingMaterial);
});

it('should register all expected routes', () => {
// Verify all controller methods are available
expect(mockController.bmMaterialsList).toBeDefined();
expect(mockController.bmPurchaseMaterials).toBeDefined();
expect(mockController.bmPostMaterialUpdateRecord).toBeDefined();
expect(mockController.bmPostMaterialUpdateBulk).toBeDefined();
expect(mockController.bmupdatePurchaseStatus).toBeDefined();
expect(mockController.bmGetMaterialCostCorrelation).toBeDefined();
expect(mockController.bmGetMaterialSummaryByProject).toBeDefined();
});
});
});
1 change: 1 addition & 0 deletions src/routes/bmdashboard/bmMaterialsRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const routes = function (buildingMaterial) {
materialsRouter.route('/updateMaterialStatus').post(controller.bmupdatePurchaseStatus);

materialsRouter.route('/materials/stock-out-risk').get(controller.bmGetMaterialStockOutRisk);
materialsRouter.route('/materials/cost-correlation').get(controller.bmGetMaterialCostCorrelation);

materialsRouter.route('/materials/:projectId').get(controller.bmGetMaterialSummaryByProject);

Expand Down
5 changes: 4 additions & 1 deletion src/startup/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,10 @@ const materialCostRouter = require('../routes/materialCostRouter')();

// bm dashboard
const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')();
const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(buildingMaterial);
// NOTE: Use buildingMaterialModel (from buildingMaterial.js, queries 'buildingMaterials' collection)
// NOT buildingMaterial (from buildingInventoryItem.js, queries 'buildingInventoryItems' collection)
// See DEBUGGING_DOCUMENTATION.md for details on this fix
const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(buildingMaterialModel);
const bmReusableRouter = require('../routes/bmdashboard/bmReusableRouter')(buildingReusable);
const bmProjectRouter = require('../routes/bmdashboard/bmProjectRouter')(buildingProject);
const bmOrgLocation = require('../routes/bmdashboard/bmOrgLocationRouter')();
Expand Down
Loading
Loading