grocery-app/backend/controllers/available-items.controller.js

323 lines
9.3 KiB
JavaScript

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_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 catalog 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 catalog 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 catalog 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 [deletedCatalogEntry, deletedClassification] = await Promise.all([
AvailableItems.deleteAvailableItem(householdId, storeId, itemId),
List.deleteClassification(householdId, storeId, itemId),
]);
if (!deletedCatalogEntry && !deletedClassification) {
return sendError(res, 404, "Managed item settings not found");
}
res.json({ message: "Store item settings cleared" });
} catch (error) {
if (isCatalogTableMissing(error)) {
return sendError(
res,
503,
"Store item catalog is unavailable until the latest database migration is applied"
);
}
logError(req, "availableItems.deleteAvailableItem", error);
sendError(res, 500, "Failed to remove available 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 catalog is unavailable until the latest database migration is applied"
);
}
logError(req, "availableItems.importCurrentItems", error);
sendError(res, 500, "Failed to import current list items");
}
};