const AvailableItems = require("../models/available-item.model"); const List = require("../models/list.model.v2"); 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 parseBoolean(value) { return value === true || value === "true" || value === "1"; } function isCatalogTableMissing(error) { return error?.code === "42P01" && /(household_store_items|household_store_available_items)/i.test(error?.message || ""); } function parseClassificationInput(value) { if (value === undefined) { return undefined; } if (value === null) { return null; } if (typeof value === "string") { const trimmed = value.trim(); if (!trimmed) { return null; } if (trimmed === "null") { return null; } if (trimmed.startsWith("{")) { try { return JSON.parse(trimmed); } catch (error) { return Symbol.for("invalid-classification-json"); } } return trimmed; } return value; } 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 }; } function validateClassification(res, classification) { if (!classification) { return false; } 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 && !isValidZone(zone)) { sendError(res, 400, "Invalid zone"); return true; } return false; } function parseItemId(value) { const parsed = Number.parseInt(String(value), 10); return Number.isInteger(parsed) && parsed > 0 ? parsed : null; } exports.getAvailableItems = async (req, res) => { try { const { householdId, storeId } = req.params; const items = await AvailableItems.listAvailableItems(householdId, storeId, req.query.query || ""); res.json({ items, catalog_ready: true }); } catch (error) { if (isCatalogTableMissing(error)) { return res.json({ items: [], catalog_ready: false, message: "Store item management is unavailable until the latest database migration is applied.", }); } logError(req, "availableItems.getAvailableItems", error); sendError(res, 500, "Failed to load available items"); } }; exports.createAvailableItem = async (req, res) => { try { const { householdId, storeId } = req.params; const { item_name } = req.body; if (!item_name || item_name.trim() === "") { return sendError(res, 400, "Item name is required"); } const parsedClassification = parseClassificationInput(req.body.classification); if (parsedClassification === Symbol.for("invalid-classification-json")) { return sendError(res, 400, "Classification payload must be valid JSON"); } const normalizedClassification = normalizeClassificationPayload(parsedClassification); if (validateClassification(res, normalizedClassification)) { return; } const imageBuffer = req.processedImage?.buffer || null; const mimeType = req.processedImage?.mimeType || null; const item = await AvailableItems.createAvailableItem( householdId, storeId, item_name, imageBuffer, mimeType ); if (normalizedClassification) { await List.upsertClassification(householdId, storeId, item.item_id, { ...normalizedClassification, confidence: 1.0, source: "user", }); } const refreshedItem = await AvailableItems.getAvailableItemById(householdId, storeId, item.item_id); res.status(201).json({ message: "Available item added", item: refreshedItem, }); } catch (error) { if (isCatalogTableMissing(error)) { return sendError( res, 503, "Store item management is unavailable until the latest database migration is applied" ); } logError(req, "availableItems.createAvailableItem", error); if (error.code === "23505") { return sendError(res, 400, "Available item already exists for this store"); } sendError(res, 500, "Failed to add available item"); } }; exports.updateAvailableItem = async (req, res) => { try { const { householdId, storeId, itemId: rawItemId } = req.params; const itemId = parseItemId(rawItemId); if (!itemId) { return sendError(res, 400, "Item ID must be a positive integer"); } const hasClassificationField = Object.prototype.hasOwnProperty.call(req.body, "classification"); const parsedClassification = parseClassificationInput(req.body.classification); if (parsedClassification === Symbol.for("invalid-classification-json")) { return sendError(res, 400, "Classification payload must be valid JSON"); } const normalizedClassification = normalizeClassificationPayload(parsedClassification); if (normalizedClassification && validateClassification(res, normalizedClassification)) { return; } const updatedItem = await AvailableItems.updateAvailableItem(householdId, storeId, itemId, { itemName: req.body.item_name, imageBuffer: req.processedImage?.buffer || null, mimeType: req.processedImage?.mimeType || null, removeImage: parseBoolean(req.body.remove_image), }); if (!updatedItem) { return sendError(res, 404, "Available item not found"); } if (hasClassificationField) { if (normalizedClassification) { await List.upsertClassification(householdId, storeId, updatedItem.item_id, { ...normalizedClassification, confidence: 1.0, source: "user", }); } else { await List.deleteClassification(householdId, storeId, updatedItem.item_id); } } const refreshedItem = await AvailableItems.getAvailableItemById( householdId, storeId, updatedItem.item_id ); res.json({ message: "Available item updated", item: refreshedItem, }); } catch (error) { if (isCatalogTableMissing(error)) { return sendError( res, 503, "Store item management is unavailable until the latest database migration is applied" ); } logError(req, "availableItems.updateAvailableItem", error); if (error.code === "23505") { return sendError(res, 400, "Available item already exists for this store"); } sendError(res, 500, "Failed to update available item"); } }; exports.deleteAvailableItem = async (req, res) => { try { const { householdId, storeId, itemId: rawItemId } = req.params; const itemId = parseItemId(rawItemId); if (!itemId) { return sendError(res, 400, "Item ID must be a positive integer"); } const deleted = await AvailableItems.deleteAvailableItem(householdId, storeId, itemId); if (!deleted) { return sendError(res, 404, "Store item not found"); } res.json({ message: "Store item deleted" }); } catch (error) { if (isCatalogTableMissing(error)) { return sendError( res, 503, "Store item management is unavailable until the latest database migration is applied" ); } logError(req, "availableItems.deleteAvailableItem", error); sendError(res, 500, "Failed to delete store item"); } }; exports.importCurrentItems = async (req, res) => { try { const { householdId, storeId } = req.params; const importedCount = await AvailableItems.importCurrentListItems(householdId, storeId); res.json({ message: importedCount > 0 ? "Imported current list items" : "No current list items to import", imported_count: importedCount, }); } catch (error) { if (isCatalogTableMissing(error)) { return sendError( res, 503, "Store item management is unavailable until the latest database migration is applied" ); } logError(req, "availableItems.importCurrentItems", error); sendError(res, 500, "Failed to import current list items"); } };