#serializeJSON({"OK": false, "ERROR": "admin_only"})# /** * Eliminate Categories Table - Schema Migration * * Final unified schema: * - Everything is an Item with ItemBusinessID * - ParentID=0 items are either categories or templates (derived from usage) * - Categories = items at ParentID=0 that have menu items as children * - Templates = items at ParentID=0 that appear in ItemTemplateLinks * - Orphans = ParentID=0 items that are neither (cleanup candidates) * * Steps: * 1. Add ItemBusinessID column to Items * 2. For each Category: create Item, re-parent menu items, set BusinessID * 3. Set ItemBusinessID on templates based on linked items * * Query param: ?dryRun=1 to preview without making changes */ response = { "OK": false, "steps": [], "migrations": [], "dryRun": false }; try { dryRun = structKeyExists(url, "dryRun") && url.dryRun == 1; response["dryRun"] = dryRun; // Step 1: Add ItemBusinessID column if it doesn't exist try { if (!dryRun) { queryExecute(" ALTER TABLE Items ADD COLUMN ItemBusinessID INT DEFAULT 0 AFTER ItemID ", {}, { datasource: "payfrit" }); } arrayAppend(response.steps, "Added ItemBusinessID column to Items table"); } catch (any e) { if (findNoCase("Duplicate column", e.message)) { arrayAppend(response.steps, "ItemBusinessID column already exists"); } else { throw(e); } } // Step 2: Add index on ItemBusinessID try { if (!dryRun) { queryExecute(" CREATE INDEX idx_item_business ON Items (ItemBusinessID) ", {}, { datasource: "payfrit" }); } arrayAppend(response.steps, "Added index on ItemBusinessID"); } catch (any e) { if (findNoCase("Duplicate key name", e.message)) { arrayAppend(response.steps, "Index idx_item_business already exists"); } else { arrayAppend(response.steps, "Index warning: " & e.message); } } // Step 3: Drop foreign key constraint on ItemCategoryID if it exists try { if (!dryRun) { queryExecute(" ALTER TABLE Items DROP FOREIGN KEY Items_ibfk_1 ", {}, { datasource: "payfrit" }); } arrayAppend(response.steps, "Dropped foreign key constraint Items_ibfk_1"); } catch (any e) { if (findNoCase("check that column", e.message) || findNoCase("Can't DROP", e.message)) { arrayAppend(response.steps, "Foreign key Items_ibfk_1 already dropped or doesn't exist"); } else { arrayAppend(response.steps, "FK warning: " & e.message); } } // Step 4: Get all Categories qCategories = queryExecute(" SELECT CategoryID, CategoryBusinessID, CategoryName FROM Categories ORDER BY CategoryBusinessID, CategoryName ", {}, { datasource: "payfrit" }); arrayAppend(response.steps, "Found " & qCategories.recordCount & " categories to migrate"); // Step 4: Migrate each category for (cat in qCategories) { migration = { "oldCategoryID": cat.CategoryID, "categoryName": cat.CategoryName, "businessID": cat.CategoryBusinessID, "newItemID": 0, "itemsUpdated": 0 }; if (!dryRun) { // Create new Item for this category (ParentID=0, no template flag needed) // Note: ItemCategoryID set to 0 temporarily until we drop that column queryExecute(" INSERT INTO Items ( ItemBusinessID, ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, ItemIsActive, ItemIsCheckedByDefault, ItemRequiresChildSelection, ItemSortOrder, ItemAddedOn ) VALUES ( :businessID, 0, :categoryName, '', 0, 0, 1, 0, 0, 0, NOW() ) ", { businessID: cat.CategoryBusinessID, categoryName: cat.CategoryName }, { datasource: "payfrit" }); // Get the new Item ID qNewItem = queryExecute(" SELECT ItemID FROM Items WHERE ItemBusinessID = :businessID AND ItemName = :categoryName AND ItemParentItemID = 0 ORDER BY ItemID DESC LIMIT 1 ", { businessID: cat.CategoryBusinessID, categoryName: cat.CategoryName }, { datasource: "payfrit" }); newItemID = qNewItem.ItemID; migration["newItemID"] = newItemID; // Update menu items in this category: // - Set ItemParentItemID = newItemID (for top-level items only) // - Set ItemBusinessID = businessID (for all items) queryExecute(" UPDATE Items SET ItemBusinessID = :businessID, ItemParentItemID = :newItemID WHERE ItemCategoryID = :categoryID AND ItemParentItemID = 0 ", { businessID: cat.CategoryBusinessID, newItemID: newItemID, categoryID: cat.CategoryID }, { datasource: "payfrit" }); // Set ItemBusinessID on ALL items in this category (including nested) queryExecute(" UPDATE Items SET ItemBusinessID = :businessID WHERE ItemCategoryID = :categoryID AND (ItemBusinessID IS NULL OR ItemBusinessID = 0) ", { businessID: cat.CategoryBusinessID, categoryID: cat.CategoryID }, { datasource: "payfrit" }); // Count how many were updated qCount = queryExecute(" SELECT COUNT(*) as cnt FROM Items WHERE ItemParentItemID = :newItemID ", { newItemID: newItemID }, { datasource: "payfrit" }); migration["itemsUpdated"] = qCount.cnt; } else { // Dry run - count what would be updated qCount = queryExecute(" SELECT COUNT(*) as cnt FROM Items WHERE ItemCategoryID = :categoryID AND ItemParentItemID = 0 ", { categoryID: cat.CategoryID }, { datasource: "payfrit" }); migration["itemsToUpdate"] = qCount.cnt; } arrayAppend(response.migrations, migration); } // Step 5: Set ItemBusinessID for templates (items in ItemTemplateLinks) // Templates get their BusinessID from the items they're linked to if (!dryRun) { queryExecute(" UPDATE Items t INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID INNER JOIN Items i ON i.ItemID = tl.ItemID SET t.ItemBusinessID = i.ItemBusinessID WHERE (t.ItemBusinessID IS NULL OR t.ItemBusinessID = 0) AND i.ItemBusinessID > 0 ", {}, { datasource: "payfrit" }); arrayAppend(response.steps, "Set ItemBusinessID on templates from linked items"); // Set ItemBusinessID on template children (options) queryExecute(" UPDATE Items c INNER JOIN Items t ON t.ItemID = c.ItemParentItemID SET c.ItemBusinessID = t.ItemBusinessID WHERE t.ItemBusinessID > 0 AND (c.ItemBusinessID IS NULL OR c.ItemBusinessID = 0) ", {}, { datasource: "payfrit" }); arrayAppend(response.steps, "Set ItemBusinessID on template children"); // Make sure templates have ParentID=0 (they live at top level) queryExecute(" UPDATE Items t INNER JOIN ItemTemplateLinks tl ON tl.TemplateItemID = t.ItemID SET t.ItemParentItemID = 0 WHERE t.ItemParentItemID != 0 ", {}, { datasource: "payfrit" }); arrayAppend(response.steps, "Ensured templates have ParentItemID=0"); } response["OK"] = true; if (dryRun) { arrayAppend(response.steps, "DRY RUN COMPLETE - No changes made. Run without ?dryRun=1 to execute."); } else { arrayAppend(response.steps, "MIGRATION COMPLETE - Categories converted to Items"); arrayAppend(response.steps, "Run cleanupCategories.cfm to drop old columns/tables after verification"); } } catch (any e) { response["ERROR"] = e.message; response["DETAIL"] = e.detail; } writeOutput(serializeJSON(response));