Portal local development: - Add BASE_PATH detection to all portal files (login, portal.js, menu-builder, station-assignment) - Allows portal to work at /biz.payfrit.com/ path locally Menu Builder fixes: - Fix duplicate template options in getForBuilder.cfm query - Filter template children by business ID with DISTINCT New APIs: - api/portal/myBusinesses.cfm - List businesses for logged-in user - api/stations/list.cfm - List KDS stations - api/menu/updateStations.cfm - Update item station assignments - api/setup/reimportBigDeans.cfm - Full Big Dean's menu import script Admin utilities: - Various debug and migration scripts for menu/template management - Beacon switching, category cleanup, modifier template setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
252 lines
9.1 KiB
Text
252 lines
9.1 KiB
Text
<cfsetting showdebugoutput="false">
|
|
<cfsetting enablecfoutputonly="true">
|
|
<cfsetting requesttimeout="300">
|
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
|
|
|
<!--- Only allow from localhost --->
|
|
<cfif NOT (cgi.remote_addr EQ "127.0.0.1" OR cgi.remote_addr EQ "::1" OR findNoCase("localhost", cgi.server_name))>
|
|
<cfoutput>#serializeJSON({"OK": false, "ERROR": "admin_only"})#</cfoutput>
|
|
<cfabort>
|
|
</cfif>
|
|
|
|
<cfscript>
|
|
/**
|
|
* 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));
|
|
</cfscript>
|