This repository has been archived on 2026-03-21. You can view files and clone it, but cannot push or open issues or pull requests.
payfrit-biz/api/admin/eliminateCategories.cfm
John Mizerek 1210249f54 Normalize database column and table names across entire codebase
Update all SQL queries, query result references, and ColdFusion code to match
the renamed database schema. Tables use plural CamelCase, PKs are all `ID`,
column prefixes stripped (e.g. BusinessName→Name, UserFirstName→FirstName).

Key changes:
- Strip table-name prefixes from all column references (Businesses, Users,
  Addresses, Hours, Menus, Categories, Items, Stations, Orders,
  OrderLineItems, Tasks, TaskCategories, TaskRatings, QuickTaskTemplates,
  ScheduledTaskDefinitions, ChatMessages, Beacons, ServicePoints, Employees,
  VisitorTrackings, ApiPerfLogs, tt_States, tt_Days, tt_AddressTypes,
  tt_OrderTypes, tt_TaskTypes)
- Rename PK references from {TableName}ID to ID in all queries
- Rewrite 7 admin beacon files to use ServicePoints.BeaconID instead of
  dropped lt_Beacon_Businesses_ServicePoints link table
- Rewrite beacon assignment files (list, save, delete) for new schema
- Fix FK references incorrectly changed to ID (OrderLineItems.OrderID,
  Categories.MenuID, Tasks.CategoryID, ServicePoints.BeaconID)
- Update Addresses: AddressLat→Latitude, AddressLng→Longitude
- Update Users: UserPassword→Password, UserIsEmailVerified→IsEmailVerified,
  UserIsActive→IsActive, UserBalance→Balance, etc.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 15:39:12 -08:00

252 lines
8.8 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 BusinessID
* - 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 lt_ItemID_TemplateItemID
* - Orphans = ParentID=0 items that are neither (cleanup candidates)
*
* Steps:
* 1. Add BusinessID column to Items
* 2. For each Category: create Item, re-parent menu items, set BusinessID
* 3. Set BusinessID 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 BusinessID column if it doesn't exist
try {
if (!dryRun) {
queryExecute("
ALTER TABLE Items ADD COLUMN BusinessID INT DEFAULT 0 AFTER ItemID
", {}, { datasource: "payfrit" });
}
arrayAppend(response.steps, "Added BusinessID column to Items table");
} catch (any e) {
if (findNoCase("Duplicate column", e.message)) {
arrayAppend(response.steps, "BusinessID column already exists");
} else {
throw(e);
}
}
// Step 2: Add index on BusinessID
try {
if (!dryRun) {
queryExecute("
CREATE INDEX idx_item_business ON Items (BusinessID)
", {}, { datasource: "payfrit" });
}
arrayAppend(response.steps, "Added index on BusinessID");
} 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 CategoryID 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 ID, BusinessID, Name
FROM Categories
ORDER BY BusinessID, Name
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Found " & qCategories.recordCount & " categories to migrate");
// Step 4: Migrate each category
for (cat in qCategories) {
migration = {
"oldCategoryID": cat.ID,
"categoryName": cat.Name,
"businessID": cat.BusinessID,
"newItemID": 0,
"itemsUpdated": 0
};
if (!dryRun) {
// Create new Item for this category (ParentID=0, no template flag needed)
// Note: CategoryID set to 0 temporarily until we drop that column
queryExecute("
INSERT INTO Items (
BusinessID,
CategoryID,
Name,
Description,
ParentItemID,
Price,
IsActive,
IsCheckedByDefault,
RequiresChildSelection,
SortOrder,
AddedOn
) VALUES (
:businessID,
0,
:categoryName,
'',
0,
0,
1,
0,
0,
0,
NOW()
)
", {
businessID: cat.BusinessID,
categoryName: cat.Name
}, { datasource: "payfrit" });
// Get the new Item ID
qNewItem = queryExecute("
SELECT ID FROM Items
WHERE BusinessID = :businessID
AND Name = :categoryName
AND ParentItemID = 0
ORDER BY ID DESC
LIMIT 1
", {
businessID: cat.BusinessID,
categoryName: cat.Name
}, { datasource: "payfrit" });
newItemID = qNewItem.ID;
migration["newItemID"] = newItemID;
// Update menu items in this category:
// - Set ParentItemID = newItemID (for top-level items only)
// - Set BusinessID = businessID (for all items)
queryExecute("
UPDATE Items
SET BusinessID = :businessID,
ParentItemID = :newItemID
WHERE CategoryID = :categoryID
AND ParentItemID = 0
", {
businessID: cat.BusinessID,
newItemID: newItemID,
categoryID: cat.ID
}, { datasource: "payfrit" });
// Set BusinessID on ALL items in this category (including nested)
queryExecute("
UPDATE Items
SET BusinessID = :businessID
WHERE CategoryID = :categoryID
AND (BusinessID IS NULL OR BusinessID = 0)
", {
businessID: cat.BusinessID,
categoryID: cat.ID
}, { datasource: "payfrit" });
// Count how many were updated
qCount = queryExecute("
SELECT COUNT(*) as cnt FROM Items
WHERE ParentItemID = :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 CategoryID = :categoryID
AND ParentItemID = 0
", { categoryID: cat.ID }, { datasource: "payfrit" });
migration["itemsToUpdate"] = qCount.cnt;
}
arrayAppend(response.migrations, migration);
}
// Step 5: Set BusinessID for templates (items in lt_ItemID_TemplateItemID)
// Templates get their BusinessID from the items they're linked to
if (!dryRun) {
queryExecute("
UPDATE Items t
INNER JOIN lt_ItemID_TemplateItemID tl ON tl.TemplateItemID = t.ItemID
INNER JOIN Items i ON i.ID = tl.ItemID
SET t.BusinessID = i.BusinessID
WHERE (t.BusinessID IS NULL OR t.BusinessID = 0)
AND i.BusinessID > 0
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Set BusinessID on templates from linked items");
// Set BusinessID on template children (options)
queryExecute("
UPDATE Items c
INNER JOIN Items t ON t.ItemID = c.ParentItemID
SET c.BusinessID = t.BusinessID
WHERE t.BusinessID > 0
AND (c.BusinessID IS NULL OR c.BusinessID = 0)
", {}, { datasource: "payfrit" });
arrayAppend(response.steps, "Set BusinessID on template children");
// Make sure templates have ParentID=0 (they live at top level)
queryExecute("
UPDATE Items t
INNER JOIN lt_ItemID_TemplateItemID tl ON tl.TemplateItemID = t.ItemID
SET t.ParentItemID = 0
WHERE t.ParentItemID != 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>