From 2ef915c7120396ce14846f4507cbaa0d8bc0fe40 Mon Sep 17 00:00:00 2001 From: somramnani Date: Thu, 9 Jul 2026 12:26:20 -0400 Subject: [PATCH] refactor(KIInventory): add update and delete routes --- .../KIInventoryController.js | 69 +++++++ .../KIInventoryController.spec.js | 170 ++++++++++++++++++ .../kitchenandinventory/KIInventoryRouter.js | 2 + 3 files changed, 241 insertions(+) diff --git a/src/controllers/kitchenandinventory/KIInventoryController.js b/src/controllers/kitchenandinventory/KIInventoryController.js index 5c3070d6c..ef88492a0 100644 --- a/src/controllers/kitchenandinventory/KIInventoryController.js +++ b/src/controllers/kitchenandinventory/KIInventoryController.js @@ -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 @@ -198,6 +265,8 @@ const KIInventoryController = () => { updateOnUsage, updateStoredQuantity, updateNextHarvest, + updateItem, + deleteItem, getInventoryStats, }; }; diff --git a/src/controllers/kitchenandinventory/KIInventoryController.spec.js b/src/controllers/kitchenandinventory/KIInventoryController.spec.js index 99058e5e5..bd0964e96 100644 --- a/src/controllers/kitchenandinventory/KIInventoryController.spec.js +++ b/src/controllers/kitchenandinventory/KIInventoryController.spec.js @@ -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; }); @@ -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(); diff --git a/src/routes/kitchenandinventory/KIInventoryRouter.js b/src/routes/kitchenandinventory/KIInventoryRouter.js index 90cbc8b23..7c8a29ad4 100644 --- a/src/routes/kitchenandinventory/KIInventoryRouter.js +++ b/src/routes/kitchenandinventory/KIInventoryRouter.js @@ -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