const List = require("../models/list.model.v2"); const householdModel = require("../models/household.model"); const { isValidItemType, isValidItemGroup, isValidZone } = require("../constants/classifications"); const { sendError } = require("../utils/http"); const { logError } = require("../utils/logger"); const LEGACY_ITEM_TYPE_MAP = { beverages: "beverage", snacks: "snack", }; function normalizeClassificationPayload(classification) { if (typeof classification === "string") { const normalizedItemType = LEGACY_ITEM_TYPE_MAP[classification] || classification; return { item_type: normalizedItemType, item_group: null, zone: null, }; } if (!classification || typeof classification !== "object" || Array.isArray(classification)) { return null; } const item_type = typeof classification.item_type === "string" && classification.item_type.trim() !== "" ? classification.item_type.trim() : null; const item_group = typeof classification.item_group === "string" && classification.item_group.trim() !== "" ? classification.item_group.trim() : null; const zone = typeof classification.zone === "string" && classification.zone.trim() !== "" ? classification.zone.trim() : null; if (!item_type && !item_group && !zone) { return null; } return { item_type, item_group, zone }; } /** * Get list items for household and store * GET /households/:householdId/stores/:storeId/list */ exports.getList = async (req, res) => { try { const { householdId, storeId } = req.params; const items = await List.getHouseholdStoreList(householdId, storeId); res.json({ items }); } catch (error) { logError(req, "listsV2.getList", error); sendError(res, 500, "Failed to get list"); } }; /** * Get specific item by name * GET /households/:householdId/stores/:storeId/list/item */ exports.getItemByName = async (req, res) => { try { const { householdId, storeId } = req.params; const { item_name } = req.query; if (!item_name) { return sendError(res, 400, "Item name is required"); } const item = await List.getItemByName(householdId, storeId, item_name); if (!item) { return sendError(res, 404, "Item not found"); } res.json(item); } catch (error) { logError(req, "listsV2.getItemByName", error); sendError(res, 500, "Failed to get item"); } }; /** * Add or update item in household store list * POST /households/:householdId/stores/:storeId/list/add */ exports.addItem = async (req, res) => { try { const { householdId, storeId } = req.params; const { item_name, quantity, notes, added_for_user_id } = req.body; const userId = req.user.id; let historyUserId = userId; if (!item_name || item_name.trim() === "") { return sendError(res, 400, "Item name is required"); } if (added_for_user_id !== undefined && added_for_user_id !== null && String(added_for_user_id).trim() !== "") { const rawAddedForUserId = String(added_for_user_id).trim(); if (!/^\d+$/.test(rawAddedForUserId)) { return sendError(res, 400, "Added-for user ID must be a positive integer"); } const parsedUserId = Number.parseInt(rawAddedForUserId, 10); if (!Number.isInteger(parsedUserId) || parsedUserId <= 0) { return sendError(res, 400, "Added-for user ID must be a positive integer"); } const isMember = await householdModel.isHouseholdMember(householdId, parsedUserId); if (!isMember) { return sendError(res, 400, "Selected user is not a member of this household"); } historyUserId = parsedUserId; } // Get processed image if uploaded const imageBuffer = req.processedImage?.buffer || null; const mimeType = req.processedImage?.mimeType || null; const result = await List.addOrUpdateItem( householdId, storeId, item_name, quantity || "1", userId, imageBuffer, mimeType, notes ); // Add history record await List.addHistoryRecord(result.listId, quantity || "1", historyUserId); res.json({ message: result.isNew ? "Item added" : "Item updated", item: { id: result.listId, item_name: result.itemName, quantity: quantity || "1", bought: false } }); } catch (error) { logError(req, "listsV2.addItem", error); sendError(res, 500, "Failed to add item"); } }; /** * Mark item as bought or unbought * PATCH /households/:householdId/stores/:storeId/list/item */ exports.markBought = async (req, res) => { try { const { householdId, storeId } = req.params; const { item_name, bought, quantity_bought } = req.body; if (!item_name) return sendError(res, 400, "Item name is required"); const item = await List.getItemByName(householdId, storeId, item_name); if (!item) return sendError(res, 404, "Item not found"); // Update bought status (with optional partial purchase) await List.setBought(item.id, bought, quantity_bought); res.json({ message: bought ? "Item marked as bought" : "Item unmarked" }); } catch (error) { logError(req, "listsV2.markBought", error); sendError(res, 500, "Failed to update item"); } }; /** * Update item details (quantity, notes) * PUT /households/:householdId/stores/:storeId/list/item */ exports.updateItem = async (req, res) => { try { const { householdId, storeId } = req.params; const { item_name, quantity, notes } = req.body; if (!item_name) { return sendError(res, 400, "Item name is required"); } // Get the list item const item = await List.getItemByName(householdId, storeId, item_name); if (!item) { return sendError(res, 404, "Item not found"); } // Update item await List.updateItem(item.id, item_name, quantity, notes); res.json({ message: "Item updated", item: { id: item.id, item_name, quantity, notes } }); } catch (error) { logError(req, "listsV2.updateItem", error); sendError(res, 500, "Failed to update item"); } }; /** * Delete item from list * DELETE /households/:householdId/stores/:storeId/list/item */ exports.deleteItem = async (req, res) => { try { const { householdId, storeId } = req.params; const { item_name } = req.body; if (!item_name) { return sendError(res, 400, "Item name is required"); } // Get the list item const item = await List.getItemByName(householdId, storeId, item_name); if (!item) { return sendError(res, 404, "Item not found"); } await List.deleteItem(item.id); res.json({ message: "Item deleted" }); } catch (error) { logError(req, "listsV2.deleteItem", error); sendError(res, 500, "Failed to delete item"); } }; /** * Get item suggestions based on query * GET /households/:householdId/stores/:storeId/list/suggestions */ exports.getSuggestions = async (req, res) => { try { const { householdId, storeId } = req.params; const { query } = req.query; const suggestions = await List.getSuggestions(query || "", householdId, storeId); res.json(suggestions); } catch (error) { logError(req, "listsV2.getSuggestions", error); sendError(res, 500, "Failed to get suggestions"); } }; /** * Get recently bought items * GET /households/:householdId/stores/:storeId/list/recent */ exports.getRecentlyBought = async (req, res) => { try { const { householdId, storeId } = req.params; const items = await List.getRecentlyBoughtItems(householdId, storeId); res.json(items); } catch (error) { logError(req, "listsV2.getRecentlyBought", error); sendError(res, 500, "Failed to get recent items"); } }; /** * Get item classification * GET /households/:householdId/stores/:storeId/list/classification */ exports.getClassification = async (req, res) => { try { const { householdId, storeId } = req.params; const { item_name } = req.query; if (!item_name) { return sendError(res, 400, "Item name is required"); } // Get item ID from name const item = await List.getItemByName(householdId, storeId, item_name); if (!item) { return res.json({ classification: null }); } const classification = await List.getClassification(householdId, storeId, item.item_id); res.json({ classification }); } catch (error) { logError(req, "listsV2.getClassification", error); sendError(res, 500, "Failed to get classification"); } }; /** * Set/update item classification * POST /households/:householdId/stores/:storeId/list/classification */ exports.setClassification = async (req, res) => { try { const { householdId, storeId } = req.params; const { item_name, classification } = req.body; if (!item_name) { return sendError(res, 400, "Item name is required"); } const normalizedClassification = normalizeClassificationPayload(classification); if (!normalizedClassification) { return sendError(res, 400, "Classification is required"); } const { item_type, item_group, zone } = normalizedClassification; if (item_type && !isValidItemType(item_type)) { return sendError(res, 400, "Invalid item_type"); } if (item_group && !item_type) { return sendError(res, 400, "Item type is required when item group is provided"); } if (item_group && !isValidItemGroup(item_type, item_group)) { return sendError(res, 400, "Invalid item_group for selected item_type"); } if (zone && !isValidZone(zone)) { return sendError(res, 400, "Invalid zone"); } // Get item - add to master items if not exists const item = await List.getItemByName(householdId, storeId, item_name); let itemId; if (!item) { // Item doesn't exist in list, need to get from items table or create const itemResult = await List.addOrUpdateItem( householdId, storeId, item_name, "1", req.user.id, null, null ); itemId = itemResult.itemId; } else { itemId = item.item_id; } await List.upsertClassification(householdId, storeId, itemId, { item_type, item_group, zone, confidence: 1.0, source: "user", }); res.json({ message: "Classification set", classification: normalizedClassification }); } catch (error) { logError(req, "listsV2.setClassification", error); sendError(res, 500, "Failed to set classification"); } }; /** * Update item image * POST /households/:householdId/stores/:storeId/list/update-image */ exports.updateItemImage = async (req, res) => { try { const { householdId, storeId } = req.params; const { item_name, quantity } = req.body; const userId = req.user.id; // Get processed image const imageBuffer = req.processedImage?.buffer || null; const mimeType = req.processedImage?.mimeType || null; if (!imageBuffer) { return sendError(res, 400, "No image provided"); } // Update the item with new image await List.addOrUpdateItem(householdId, storeId, item_name, quantity, userId, imageBuffer, mimeType); res.json({ message: "Image updated successfully" }); } catch (error) { logError(req, "listsV2.updateItemImage", error); sendError(res, 500, "Failed to update image"); } };