Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
69 changes: 69 additions & 0 deletions src/controllers/kitchenandinventory/KIInventoryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,73 @@ const KIInventoryController = () => {
res.status(400).json({ message: err.message });
}
};
const updateItem = async (req, res) => {
try {
const { itemId } = req.params;
const item = await KIInventoryItem.findById(itemId);

if (!item) {
return res.status(404).json({ message: 'Item not found.' });
}

const allowedFields = [
'name',
'type',
'unit',
'location',
'category',
'onsite',
'presentQuantity',
'storedQuantity',
'reorderAt',
'monthlyUsage',
'expiryDate',
'lastHarvestDate',
'nextHarvestDate',
'nextHarvestQuantity',
];

allowedFields.forEach((field) => {
if (Object.prototype.hasOwnProperty.call(req.body, field)) {
item[field] = req.body[field] === '' ? undefined : req.body[field];
}
});

if (item.storedQuantity < item.presentQuantity) {
return res.status(400).json({
message: 'Stored quantity must be greater than or equal to current stock.',
});
}

item.updatedAt = new Date();

const savedItem = await item.save();

return res.status(200).json({
message: 'Inventory item updated successfully.',
data: savedItem,
});
} catch (err) {
return res.status(400).json({ message: err.message });
}
};
const deleteItem = async (req, res) => {
try {
const { itemId } = req.params;
const deletedItem = await KIInventoryItem.findByIdAndDelete(itemId);

if (!deletedItem) {
return res.status(404).json({ message: 'Item not found.' });
}

return res.status(200).json({
message: 'Inventory item deleted successfully.',
data: deletedItem,
});
} catch (err) {
return res.status(400).json({ message: err.message });
}
};
const getInventoryStats = async (req, res) => {
try {
// Only fetch the fields needed for stats — avoids sending full item payloads
Expand Down Expand Up @@ -198,6 +265,8 @@ const KIInventoryController = () => {
updateOnUsage,
updateStoredQuantity,
updateNextHarvest,
updateItem,
deleteItem,
getInventoryStats,
};
};
Expand Down
170 changes: 170 additions & 0 deletions src/controllers/kitchenandinventory/KIInventoryController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ jest.mock('../../models/kitchenandinventory/KIInventoryItems', () => {
const mock = jest.fn();
mock.find = jest.fn();
mock.findById = jest.fn();
mock.findByIdAndDelete = jest.fn();
return mock;
});

Expand Down Expand Up @@ -442,6 +443,175 @@ describe('KIInventoryController', () => {
});
});

describe('updateItem', () => {
test('updates allowed fields and converts empty strings to undefined', async () => {
const item = {
name: 'Old Apples',
type: 'fruit',
unit: 'lbs',
location: 'Pantry',
category: 'INGREDIENT',
onsite: true,
presentQuantity: 8,
storedQuantity: 12,
reorderAt: 5,
monthlyUsage: 3,
expiryDate: '2030-01-01',
lastHarvestDate: '2029-01-01',
nextHarvestDate: '2030-02-01',
nextHarvestQuantity: 10,
save: jest.fn(),
};
item.save.mockResolvedValue(item);
KIInventoryItem.findById.mockResolvedValue(item);

const req = makeReq({
params: { itemId: VALID_OID },
body: {
name: 'Fresh Apples',
type: 'fresh fruit',
unit: 'kg',
location: '',
category: 'INGREDIENT',
onsite: false,
presentQuantity: 10,
storedQuantity: 20,
reorderAt: 7,
monthlyUsage: 4,
expiryDate: '2031-01-01',
lastHarvestDate: '',
nextHarvestDate: '2031-02-01',
nextHarvestQuantity: 15,
ignoredField: 'do not save',
},
});
const res = makeRes();

await controller.updateItem(req, res);

expect(KIInventoryItem.findById).toHaveBeenCalledWith(VALID_OID);
expect(item.name).toBe('Fresh Apples');
expect(item.type).toBe('fresh fruit');
expect(item.unit).toBe('kg');
expect(item.location).toBeUndefined();
expect(item.category).toBe('INGREDIENT');
expect(item.onsite).toBe(false);
expect(item.presentQuantity).toBe(10);
expect(item.storedQuantity).toBe(20);
expect(item.reorderAt).toBe(7);
expect(item.monthlyUsage).toBe(4);
expect(item.expiryDate).toBe('2031-01-01');
expect(item.lastHarvestDate).toBeUndefined();
expect(item.nextHarvestDate).toBe('2031-02-01');
expect(item.nextHarvestQuantity).toBe(15);
expect(item.ignoredField).toBeUndefined();
expect(item.updatedAt).toEqual(expect.any(Date));
expect(item.save).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
message: 'Inventory item updated successfully.',
data: item,
});
});

test('returns 404 if item not found', async () => {
KIInventoryItem.findById.mockResolvedValue(null);

const req = makeReq({ params: { itemId: VALID_OID }, body: { name: 'Missing Item' } });
const res = makeRes();

await controller.updateItem(req, res);

expect(KIInventoryItem.findById).toHaveBeenCalledWith(VALID_OID);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ message: 'Item not found.' });
});

test('returns 400 when stored quantity is less than present quantity', async () => {
const item = {
storedQuantity: 10,
presentQuantity: 5,
save: jest.fn(),
};
KIInventoryItem.findById.mockResolvedValue(item);

const req = makeReq({
params: { itemId: VALID_OID },
body: { storedQuantity: 8, presentQuantity: 9 },
});
const res = makeRes();

await controller.updateItem(req, res);

expect(item.save).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({
message: 'Stored quantity must be greater than or equal to current stock.',
});
});

test('returns 400 on save error', async () => {
const item = {
storedQuantity: 10,
presentQuantity: 5,
save: jest.fn().mockRejectedValue(new Error('Save Error')),
};
KIInventoryItem.findById.mockResolvedValue(item);

const req = makeReq({ params: { itemId: VALID_OID }, body: { name: 'Fresh Apples' } });
const res = makeRes();

await controller.updateItem(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ message: 'Save Error' });
});
});

describe('deleteItem', () => {
test('successfully deletes an item', async () => {
const deletedItem = { _id: VALID_OID, name: 'Apples' };
KIInventoryItem.findByIdAndDelete.mockResolvedValue(deletedItem);

const req = makeReq({ params: { itemId: VALID_OID } });
const res = makeRes();

await controller.deleteItem(req, res);

expect(KIInventoryItem.findByIdAndDelete).toHaveBeenCalledWith(VALID_OID);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
message: 'Inventory item deleted successfully.',
data: deletedItem,
});
});

test('returns 404 if item not found', async () => {
KIInventoryItem.findByIdAndDelete.mockResolvedValue(null);

const req = makeReq({ params: { itemId: VALID_OID } });
const res = makeRes();

await controller.deleteItem(req, res);

expect(KIInventoryItem.findByIdAndDelete).toHaveBeenCalledWith(VALID_OID);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ message: 'Item not found.' });
});

test('returns 400 on DB error', async () => {
KIInventoryItem.findByIdAndDelete.mockRejectedValue(new Error('DB Error'));

const req = makeReq({ params: { itemId: VALID_OID } });
const res = makeRes();

await controller.deleteItem(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ message: 'DB Error' });
});
});

describe('getInventoryStats', () => {
test('successfully returns calculated stats including preserved stock', async () => {
const farFuture = new Date();
Expand Down
2 changes: 2 additions & 0 deletions src/routes/kitchenandinventory/KIInventoryRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const router = function () {
inventoryRouter.route('/items/storedQuantity').post(controller.updateStoredQuantity); // Add new stock
inventoryRouter.route('/items/nextHarvest').put(controller.updateNextHarvest); // Update next harvest details

inventoryRouter.route('/items/:itemId').put(controller.updateItem).delete(controller.deleteItem); // Update Item

// ── Wildcard route (must be last to avoid shadowing named routes above) ───
inventoryRouter.route('/items/:category').get(controller.getItemsByCategory); // Get items by category

Expand Down
Loading