jest.mock("../models/list.model.v2", () => ({ addHistoryRecord: jest.fn(), addOrUpdateItem: jest.fn(), getItemByName: jest.fn(), upsertClassification: jest.fn(), })); jest.mock("../models/household.model", () => ({ isHouseholdMember: jest.fn(), })); jest.mock("../utils/logger", () => ({ logError: jest.fn(), })); const List = require("../models/list.model.v2"); const householdModel = require("../models/household.model"); const controller = require("../controllers/lists.controller.v2"); function createResponse() { const res = {}; res.status = jest.fn().mockReturnValue(res); res.json = jest.fn().mockReturnValue(res); return res; } describe("lists.controller.v2 addItem", () => { beforeEach(() => { jest.clearAllMocks(); List.addOrUpdateItem.mockResolvedValue({ listId: 42, itemId: 99, itemName: "milk", isNew: true, }); List.addHistoryRecord.mockResolvedValue(undefined); List.getItemByName.mockResolvedValue({ id: 42, item_id: 99, item_name: "milk" }); List.upsertClassification.mockResolvedValue(undefined); householdModel.isHouseholdMember.mockResolvedValue(true); }); test("records history for selected added_for_user_id when member is valid", async () => { const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", quantity: "1", added_for_user_id: "9" }, user: { id: 7 }, processedImage: null, }; const res = createResponse(); await controller.addItem(req, res); expect(householdModel.isHouseholdMember).toHaveBeenCalledWith("1", 9); expect(List.addOrUpdateItem).toHaveBeenCalled(); expect(List.addHistoryRecord).toHaveBeenCalledWith(42, "1", 9); expect(res.status).not.toHaveBeenCalledWith(400); }); test("records history using request user when added_for_user_id is not provided", async () => { const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", quantity: "1" }, user: { id: 7 }, processedImage: null, }; const res = createResponse(); await controller.addItem(req, res); expect(householdModel.isHouseholdMember).not.toHaveBeenCalled(); expect(List.addOrUpdateItem).toHaveBeenCalled(); expect(List.addHistoryRecord).toHaveBeenCalledWith(42, "1", 7); expect(res.status).not.toHaveBeenCalledWith(400); }); test("records history using request user when added_for_user_id is blank", async () => { const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", quantity: "1", added_for_user_id: " " }, user: { id: 7 }, processedImage: null, }; const res = createResponse(); await controller.addItem(req, res); expect(householdModel.isHouseholdMember).not.toHaveBeenCalled(); expect(List.addOrUpdateItem).toHaveBeenCalled(); expect(List.addHistoryRecord).toHaveBeenCalledWith(42, "1", 7); expect(res.status).not.toHaveBeenCalledWith(400); }); test("rejects invalid added_for_user_id", async () => { const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", quantity: "1", added_for_user_id: "abc" }, user: { id: 7 }, processedImage: null, }; const res = createResponse(); await controller.addItem(req, res); expect(List.addOrUpdateItem).not.toHaveBeenCalled(); expect(List.addHistoryRecord).not.toHaveBeenCalled(); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ error: expect.objectContaining({ message: "Added-for user ID must be a positive integer", }), }) ); }); test("rejects malformed numeric-looking added_for_user_id", async () => { const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", quantity: "1", added_for_user_id: "9abc" }, user: { id: 7 }, processedImage: null, }; const res = createResponse(); await controller.addItem(req, res); expect(List.addOrUpdateItem).not.toHaveBeenCalled(); expect(List.addHistoryRecord).not.toHaveBeenCalled(); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ error: expect.objectContaining({ message: "Added-for user ID must be a positive integer", }), }) ); }); test("rejects added_for_user_id when target user is not household member", async () => { householdModel.isHouseholdMember.mockResolvedValue(false); const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", quantity: "1", added_for_user_id: "11" }, user: { id: 7 }, processedImage: null, }; const res = createResponse(); await controller.addItem(req, res); expect(householdModel.isHouseholdMember).toHaveBeenCalledWith("1", 11); expect(List.addOrUpdateItem).not.toHaveBeenCalled(); expect(List.addHistoryRecord).not.toHaveBeenCalled(); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ error: expect.objectContaining({ message: "Selected user is not a member of this household", }), }) ); }); }); describe("lists.controller.v2 setClassification", () => { beforeEach(() => { jest.clearAllMocks(); List.getItemByName.mockResolvedValue({ id: 42, item_id: 99, item_name: "milk" }); List.upsertClassification.mockResolvedValue(undefined); List.addOrUpdateItem.mockResolvedValue({ listId: 42, itemId: 99, itemName: "milk", isNew: true, }); }); test("accepts object classification with type, group, and zone", async () => { const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", classification: { item_type: "dairy", item_group: "Milk", zone: "Dairy & Refrigerated", }, }, user: { id: 7 }, }; const res = createResponse(); await controller.setClassification(req, res); expect(List.upsertClassification).toHaveBeenCalledWith( "1", "2", 99, expect.objectContaining({ item_type: "dairy", item_group: "Milk", zone: "Dairy & Refrigerated", confidence: 1.0, source: "user", }) ); expect(res.status).not.toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ message: "Classification set", classification: { item_type: "dairy", item_group: "Milk", zone: "Dairy & Refrigerated", }, }) ); }); test("accepts zone-only classification updates", async () => { const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", classification: { zone: "Checkout Area", }, }, user: { id: 7 }, }; const res = createResponse(); await controller.setClassification(req, res); expect(List.upsertClassification).toHaveBeenCalledWith( "1", "2", 99, expect.objectContaining({ item_type: null, item_group: null, zone: "Checkout Area", }) ); expect(res.status).not.toHaveBeenCalledWith(400); }); test("rejects invalid item_type", async () => { const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", classification: { item_type: "invalid-type", }, }, user: { id: 7 }, }; const res = createResponse(); await controller.setClassification(req, res); expect(List.upsertClassification).not.toHaveBeenCalled(); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ error: expect.objectContaining({ message: "Invalid item_type", }), }) ); }); test("rejects invalid item_group for selected item_type", async () => { const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", classification: { item_type: "dairy", item_group: "Bread", }, }, user: { id: 7 }, }; const res = createResponse(); await controller.setClassification(req, res); expect(List.upsertClassification).not.toHaveBeenCalled(); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ error: expect.objectContaining({ message: "Invalid item_group for selected item_type", }), }) ); }); test("rejects invalid zone", async () => { const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", classification: { zone: "Space Aisle", }, }, user: { id: 7 }, }; const res = createResponse(); await controller.setClassification(req, res); expect(List.upsertClassification).not.toHaveBeenCalled(); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ error: expect.objectContaining({ message: "Invalid zone", }), }) ); }); test("accepts legacy string classification values", async () => { const req = { params: { householdId: "1", storeId: "2" }, body: { item_name: "milk", classification: "beverages", }, user: { id: 7 }, }; const res = createResponse(); await controller.setClassification(req, res); expect(List.upsertClassification).toHaveBeenCalledWith( "1", "2", 99, expect.objectContaining({ item_type: "beverage", item_group: null, zone: null, }) ); expect(res.status).not.toHaveBeenCalledWith(400); }); });