const List = require("../models/list.model.v2"); const householdModel = require("../models/household.model"); const { isValidItemType, isValidItemGroup } = require("../constants/classifications"); const { sendError } = require("../utils/http"); const { logError } = require("../utils/logger"); const LEGACY_ITEM_TYPE_MAP = { beverages: "beverage", snacks: "snack", }; function getStoreLocationId(req) { return req.params.locationId || req.params.storeId; } 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 }; } async function validateClassification(res, householdId, storeLocationId, classification) { const { item_type, item_group, zone } = classification; if (item_type && !isValidItemType(item_type)) { sendError(res, 400, "Invalid item_type"); return true; } if (item_group && !item_type) { sendError(res, 400, "Item type is required when item group is provided"); return true; } if (item_group && !isValidItemGroup(item_type, item_group)) { sendError(res, 400, "Invalid item_group for selected item_type"); return true; } if (zone) { const zoneRecord = await List.getZoneByName(householdId, storeLocationId, zone); if (!zoneRecord) { sendError(res, 400, "Invalid zone"); return true; } } return false; } exports.getList = async (req, res) => { try { const { householdId } = req.params; const storeLocationId = getStoreLocationId(req); const items = await List.getHouseholdStoreList(householdId, storeLocationId); res.json({ items }); } catch (error) { logError(req, "listsV2.getList", error); sendError(res, 500, "Failed to get list"); } }; exports.getItemByName = async (req, res) => { try { const { householdId } = req.params; const storeLocationId = getStoreLocationId(req); const { item_name } = req.query; if (!item_name) { return sendError(res, 400, "Item name is required"); } const item = await List.getItemByName(householdId, storeLocationId, 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"); } }; exports.addItem = async (req, res) => { try { const { householdId } = req.params; const storeLocationId = getStoreLocationId(req); 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; } const imageBuffer = req.processedImage?.buffer || null; const mimeType = req.processedImage?.mimeType || null; const result = await List.addOrUpdateItem( householdId, storeLocationId, item_name, quantity || "1", userId, imageBuffer, mimeType, notes ); await List.addHistoryRecord( result.listId, result.householdStoreItemId, result.historyQuantity ?? quantity ?? "1", historyUserId, storeLocationId ); await List.recordItemEvent({ householdId, storeLocationId, householdStoreItemId: result.householdStoreItemId, householdListId: result.listId, actorUserId: historyUserId, eventType: "ITEM_ADDED", quantityDelta: result.historyQuantity ?? Number.parseInt(quantity || "1", 10), quantityAfter: result.quantity, metadata: { item_name: result.itemName, is_new_list_item: result.isNew, added_by_request_user_id: userId, }, }); res.json({ message: result.isNew ? "Item added" : "Item updated", item: { id: result.listId, item_name: result.itemName, quantity: result.quantity ?? quantity ?? "1", bought: false, }, }); } catch (error) { logError(req, "listsV2.addItem", error); sendError(res, 500, "Failed to add item"); } }; exports.markBought = async (req, res) => { try { const { householdId } = req.params; const storeLocationId = getStoreLocationId(req); 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, storeLocationId, item_name); if (!item) return sendError(res, 404, "Item not found"); const eventDetails = await List.setBought(item.id, bought, quantity_bought); if (eventDetails) { await List.recordItemEvent({ householdId, storeLocationId, householdStoreItemId: item.household_store_item_id, householdListId: item.id, actorUserId: req.user.id, eventType: eventDetails.eventType, quantityDelta: eventDetails.quantityDelta, quantityAfter: eventDetails.quantityAfter, metadata: { item_name, requested_quantity: quantity_bought || null, }, }); } res.json({ message: bought ? "Item marked as bought" : "Item unmarked" }); } catch (error) { logError(req, "listsV2.markBought", error); sendError(res, 500, "Failed to update item"); } }; exports.updateItem = async (req, res) => { try { const { householdId } = req.params; const storeLocationId = getStoreLocationId(req); const { item_name, quantity, notes } = req.body; if (!item_name) { return sendError(res, 400, "Item name is required"); } const item = await List.getItemByName(householdId, storeLocationId, item_name); if (!item) { return sendError(res, 404, "Item not found"); } const updateResult = await List.updateItem(item.id, item_name, quantity, notes); if (!updateResult) { return sendError(res, 404, "Item not found"); } if (quantity !== undefined && Number(quantity) !== Number(updateResult.previous.quantity)) { await List.recordItemEvent({ householdId, storeLocationId, householdStoreItemId: item.household_store_item_id, householdListId: item.id, actorUserId: req.user.id, eventType: "ITEM_QUANTITY_CHANGED", quantityDelta: Number(quantity) - Number(updateResult.previous.quantity), quantityAfter: Number(quantity), metadata: { item_name, previous_quantity: updateResult.previous.quantity, }, }); } 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"); } }; exports.deleteItem = async (req, res) => { try { const { householdId } = req.params; const storeLocationId = getStoreLocationId(req); const { item_name } = req.body; if (!item_name) { return sendError(res, 400, "Item name is required"); } const item = await List.getItemByName(householdId, storeLocationId, item_name); if (!item) { return sendError(res, 404, "Item not found"); } const deleted = await List.deleteItem(item.id); if (deleted) { await List.recordItemEvent({ householdId, storeLocationId, householdStoreItemId: item.household_store_item_id, householdListId: item.id, actorUserId: req.user.id, eventType: "ITEM_DELETED", quantityDelta: -Number(item.quantity || 0), quantityAfter: 0, metadata: { item_name }, }); } res.json({ message: "Item deleted" }); } catch (error) { logError(req, "listsV2.deleteItem", error); sendError(res, 500, "Failed to delete item"); } }; exports.getSuggestions = async (req, res) => { try { const { householdId } = req.params; const storeLocationId = getStoreLocationId(req); const { query } = req.query; const suggestions = await List.getSuggestions(query || "", householdId, storeLocationId); res.json(suggestions); } catch (error) { logError(req, "listsV2.getSuggestions", error); sendError(res, 500, "Failed to get suggestions"); } }; exports.getRecentlyBought = async (req, res) => { try { const { householdId } = req.params; const storeLocationId = getStoreLocationId(req); const items = await List.getRecentlyBoughtItems(householdId, storeLocationId); res.json(items); } catch (error) { logError(req, "listsV2.getRecentlyBought", error); sendError(res, 500, "Failed to get recent items"); } }; exports.getClassification = async (req, res) => { try { const { householdId } = req.params; const storeLocationId = getStoreLocationId(req); const { item_name } = req.query; if (!item_name) { return sendError(res, 400, "Item name is required"); } const item = await List.getItemByName(householdId, storeLocationId, item_name); if (!item) { return res.json({ classification: null }); } const classification = await List.getClassification( householdId, storeLocationId, item.item_id ); res.json({ classification }); } catch (error) { logError(req, "listsV2.getClassification", error); sendError(res, 500, "Failed to get classification"); } }; exports.setClassification = async (req, res) => { try { const { householdId } = req.params; const storeLocationId = getStoreLocationId(req); 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"); } if (await validateClassification(res, householdId, storeLocationId, normalizedClassification)) { return; } const item = await List.getItemByName(householdId, storeLocationId, item_name); let itemId; if (!item) { const itemResult = await List.ensureHouseholdStoreItem( householdId, storeLocationId, item_name ); itemId = itemResult.id; } else { itemId = item.item_id; } const updated = await List.upsertClassification(householdId, storeLocationId, itemId, { ...normalizedClassification, confidence: 1.0, source: "user", }); await List.recordItemEvent({ householdId, storeLocationId, householdStoreItemId: itemId, householdListId: item?.id || null, actorUserId: req.user.id, eventType: "ITEM_CLASSIFICATION_CHANGED", metadata: { item_name, item_type: normalizedClassification.item_type, item_group: normalizedClassification.item_group, zone: normalizedClassification.zone, }, }); if (normalizedClassification.zone) { await List.recordItemEvent({ householdId, storeLocationId, householdStoreItemId: itemId, householdListId: item?.id || null, actorUserId: req.user.id, eventType: "ITEM_ZONE_CHANGED", metadata: { item_name, zone: normalizedClassification.zone, zone_id: updated.zone_id || null, }, }); } res.json({ message: "Classification set", classification: normalizedClassification }); } catch (error) { logError(req, "listsV2.setClassification", error); sendError(res, 500, "Failed to set classification"); } }; exports.updateItemImage = async (req, res) => { try { const { householdId } = req.params; const storeLocationId = getStoreLocationId(req); const { item_name, quantity } = req.body; const userId = req.user.id; const imageBuffer = req.processedImage?.buffer || null; const mimeType = req.processedImage?.mimeType || null; if (!imageBuffer) { return sendError(res, 400, "No image provided"); } await List.addOrUpdateItem( householdId, storeLocationId, 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"); } };