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/setup/saveWizard.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

586 lines
25 KiB
Text

<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfsetting requesttimeout="300">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
/**
* Save Wizard Data
*
* Takes the extracted menu data from the setup wizard and saves it to the database.
* This transforms the wizard format into the format expected by the import system.
*
* POST JSON:
* {
* "businessId": "existing-business-id",
* "data": {
* "business": { "name": "...", "address": "...", "phone": "...", "hours": "..." },
* "categories": [ { "name": "...", "itemCount": 0 } ],
* "modifiers": [ { "name": "...", "required": true, "options": [...] } ],
* "items": [ { "name": "...", "price": 0, "category": "...", "modifiers": [...] } ]
* }
* }
*/
response = { "OK": false, "steps": [], "errors": [] };
try {
requestBody = toString(getHttpRequestData().content);
if (!len(requestBody)) {
throw(message="No request body provided");
}
data = deserializeJSON(requestBody);
businessId = structKeyExists(data, "businessId") ? val(data.businessId) : 0;
userId = structKeyExists(data, "userId") ? val(data.userId) : 0;
providedMenuId = structKeyExists(data, "menuId") ? val(data.menuId) : 0;
wizardData = structKeyExists(data, "data") ? data.data : {};
biz = structKeyExists(wizardData, "business") ? wizardData.business : {};
// If no businessId, create a new business
if (businessId == 0) {
response.steps.append("No businessId provided - creating new business");
bizName = structKeyExists(biz, "name") && isSimpleValue(biz.name) ? biz.name : "";
if (!len(bizName)) {
throw(message="Business name is required to create new business");
}
if (userId == 0) {
throw(message="userId is required to create new business");
}
// Extract phone number
bizPhone = structKeyExists(biz, "phone") && isSimpleValue(biz.phone) ? trim(biz.phone) : "";
// Extract tax rate (stored as decimal, e.g. 8.25% -> 0.0825)
bizTaxRate = structKeyExists(biz, "taxRatePercent") && isSimpleValue(biz.taxRatePercent) ? val(biz.taxRatePercent) / 100 : 0;
// Create address record first (use extracted address fields) - safely extract as simple values
addressLine1 = structKeyExists(biz, "addressLine1") && isSimpleValue(biz.addressLine1) ? trim(biz.addressLine1) : "";
city = structKeyExists(biz, "city") && isSimpleValue(biz.city) ? trim(biz.city) : "";
state = structKeyExists(biz, "state") && isSimpleValue(biz.state) ? trim(biz.state) : "";
zip = structKeyExists(biz, "zip") && isSimpleValue(biz.zip) ? trim(biz.zip) : "";
// Clean up city - remove trailing punctuation (commas, periods, etc.)
city = reReplace(city, "[,.\s]+$", "", "all");
// Look up state ID from state abbreviation
stateID = 0;
response.steps.append("State value received: '" & state & "' (len: " & len(state) & ")");
if (len(state)) {
qState = queryExecute("
SELECT tt_StateID FROM tt_States WHERE Abbreviation = :abbr
", { abbr: uCase(state) }, { datasource: "payfrit" });
response.steps.append("State lookup for '" & uCase(state) & "' found " & qState.recordCount & " records");
if (qState.recordCount > 0) {
stateID = qState.tt_StateID;
response.steps.append("Using stateID: " & stateID);
}
}
queryExecute("
INSERT INTO Addresses (Line1, City, StateID, ZIPCode, UserID, AddressTypeID, AddedOn)
VALUES (:line1, :city, :stateID, :zip, :userID, :typeID, NOW())
", {
line1: len(addressLine1) ? addressLine1 : "Address pending",
city: len(city) ? city : "",
stateID: { value = stateID > 0 ? stateID : javaCast("null", ""), cfsqltype = "cf_sql_integer", null = stateID == 0 },
zip: len(zip) ? zip : "",
userID: userId,
typeID: 2
}, { datasource: "payfrit" });
qNewAddr = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
addressId = qNewAddr.id;
response.steps.append("Created address record (ID: " & addressId & ")");
// Get community meal type (1=provide meals, 2=food bank donation)
communityMealType = structKeyExists(wizardData, "communityMealType") && isSimpleValue(wizardData.communityMealType) ? val(wizardData.communityMealType) : 1;
if (communityMealType < 1 || communityMealType > 2) communityMealType = 1;
// Create new business with address link and phone
queryExecute("
INSERT INTO Businesses (Name, Phone, UserID, AddressID, BusinessDeliveryZipCodes, CommunityMealType, TaxRate, AddedOn)
VALUES (:name, :phone, :userId, :addressId, :deliveryZips, :communityMealType, :taxRate, NOW())
", {
name: bizName,
phone: bizPhone,
userId: userId,
addressId: addressId,
deliveryZips: len(zip) ? zip : "",
communityMealType: communityMealType,
taxRate: { value: bizTaxRate, cfsqltype: "cf_sql_decimal" }
}, { datasource: "payfrit" });
qNewBiz = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
businessId = qNewBiz.id;
response.steps.append("Created new business: " & bizName & " (ID: " & businessId & ")");
// Update address with business ID link
queryExecute("
UPDATE Addresses SET BusinessID = :businessID WHERE ID = :addressID
", {
businessID: businessId,
addressID: addressId
}, { datasource: "payfrit" });
response.steps.append("Linked address to business");
// Create default task types for the business
// 1. Call Server (notifications icon, purple)
// 2. Chat With Staff (chat icon, blue)
// 3. Pay With Cash (payments icon, green)
defaultTaskTypes = [
{ name: "Call Server", icon: "notifications", color: "##9C27B0", description: "Request server assistance" },
{ name: "Chat With Staff", icon: "chat", color: "##2196F3", description: "Open a chat conversation" },
{ name: "Pay With Cash", icon: "payments", color: "##4CAF50", description: "Request to pay with cash" }
];
for (tt = 1; tt <= arrayLen(defaultTaskTypes); tt++) {
taskType = defaultTaskTypes[tt];
queryExecute("
INSERT INTO tt_TaskTypes (Name, Description, Icon, Color, BusinessID, SortOrder)
VALUES (:name, :description, :icon, :color, :businessID, :sortOrder)
", {
name: { value: taskType.name, cfsqltype: "cf_sql_varchar" },
description: { value: taskType.description, cfsqltype: "cf_sql_varchar" },
icon: { value: taskType.icon, cfsqltype: "cf_sql_varchar" },
color: { value: taskType.color, cfsqltype: "cf_sql_varchar" },
businessID: { value: businessId, cfsqltype: "cf_sql_integer" },
sortOrder: { value: tt, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
}
response.steps.append("Created 3 default task types (Call Server, Chat With Staff, Pay With Cash)");
// Save business hours from structured schedule
if (structKeyExists(biz, "hoursSchedule") && isArray(biz.hoursSchedule)) {
hoursSchedule = biz.hoursSchedule;
response.steps.append("Processing " & arrayLen(hoursSchedule) & " days of hours");
for (i = 1; i <= arrayLen(hoursSchedule); i++) {
dayData = hoursSchedule[i];
if (!isStruct(dayData)) continue;
dayID = structKeyExists(dayData, "dayId") ? val(dayData.dayId) : 0;
openTime = structKeyExists(dayData, "open") && isSimpleValue(dayData.open) ? dayData.open : "09:00";
closeTime = structKeyExists(dayData, "close") && isSimpleValue(dayData.close) ? dayData.close : "17:00";
// Convert HH:MM to HH:MM:SS if needed
if (len(openTime) == 5) openTime = openTime & ":00";
if (len(closeTime) == 5) closeTime = closeTime & ":00";
// Insert hours record for this day
queryExecute("
INSERT INTO Hours (BusinessID, DayID, OpenTime, ClosingTime)
VALUES (:bizID, :dayID, :openTime, :closeTime)
", {
bizID: businessId,
dayID: dayID,
openTime: openTime,
closeTime: closeTime
}, { datasource: "payfrit" });
}
response.steps.append("Created " & arrayLen(hoursSchedule) & " hours records");
}
} else {
// Verify existing business exists
qBiz = queryExecute("
SELECT ID, Name FROM Businesses WHERE ID = :id
", { id: businessId }, { datasource: "payfrit" });
if (qBiz.recordCount == 0) {
throw(message="Business not found: " & businessId);
}
response.steps.append("Found existing business: " & qBiz.Name);
}
// Build modifier template map
// The wizard format has modifiers as simple objects, we need to create IDs
modTemplates = structKeyExists(wizardData, "modifiers") ? wizardData.modifiers : [];
templateMap = {}; // Maps modifier name to database ItemID
response.steps.append("Processing " & arrayLen(modTemplates) & " modifier templates...");
for (i = 1; i <= arrayLen(modTemplates); i++) {
tmpl = modTemplates[i];
tmplName = structKeyExists(tmpl, "name") && isSimpleValue(tmpl.name) ? tmpl.name : "";
if (!len(tmplName)) {
response.steps.append("Warning: Skipping modifier template with no name at index " & i);
continue;
}
required = structKeyExists(tmpl, "required") && tmpl.required == true;
options = structKeyExists(tmpl, "options") && isArray(tmpl.options) ? tmpl.options : [];
// Debug: Log options info
response.steps.append("Template '" & tmplName & "' has " & arrayLen(options) & " options (type: " & (isArray(options) ? "array" : "other") & ")");
// Check if template already exists for this business
qTmpl = queryExecute("
SELECT i.ID FROM Items i
WHERE i.BusinessID = :bizID
AND i.Name = :name
AND i.ParentItemID = 0
AND i.CategoryID = 0
", { bizID: businessId, name: tmplName }, { datasource: "payfrit" });
if (qTmpl.recordCount > 0) {
templateItemID = qTmpl.ItemID;
response.steps.append("Template exists: " & tmplName & " (ID: " & templateItemID & ")");
} else {
// Create template as Item with CategoryID=0 to mark as template
queryExecute("
INSERT INTO Items (
BusinessID, Name, ParentItemID, CategoryID, Price,
IsActive, RequiresChildSelection, MaxNumSelectionReq,
SortOrder
) VALUES (
:bizID, :name, 0, 0, 0, 1, :required, 1, 0
)
", {
bizID: businessId,
name: tmplName,
required: required ? 1 : 0
}, { datasource: "payfrit" });
qNewTmpl = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
templateItemID = qNewTmpl.id;
response.steps.append("Created template: " & tmplName & " (ID: " & templateItemID & ")");
}
templateMap[tmplName] = templateItemID;
// Create/update template options
optionOrder = 1;
for (j = 1; j <= arrayLen(options); j++) {
opt = options[j];
// Safety check: ensure opt is a struct with a name that's a simple value
if (!isStruct(opt)) continue;
if (!structKeyExists(opt, "name")) continue;
if (!isSimpleValue(opt.name)) continue;
if (!len(opt.name)) continue;
optName = opt.name;
optPrice = structKeyExists(opt, "price") && isSimpleValue(opt.price) ? val(opt.price) : 0;
qOpt = queryExecute("
SELECT ID FROM Items
WHERE BusinessID = :bizID AND Name = :name AND ParentItemID = :parentID
", { bizID: businessId, name: optName, parentID: templateItemID }, { datasource: "payfrit" });
if (qOpt.recordCount == 0) {
queryExecute("
INSERT INTO Items (
BusinessID, Name, ParentItemID, CategoryID,
Price, IsActive, SortOrder
) VALUES (
:bizID, :name, :parentID, 0, :price, 1, :sortOrder
)
", {
bizID: businessId,
name: optName,
parentID: templateItemID,
price: optPrice,
sortOrder: optionOrder
}, { datasource: "payfrit" });
}
optionOrder++;
}
}
// Use provided menuId (add-menu mode) or create/find menu
if (providedMenuId > 0) {
menuID = providedMenuId;
// Look up the menu name for logging
qName = queryExecute("SELECT Name FROM Menus WHERE ID = :id", { id: menuID }, { datasource: "payfrit" });
menuName = qName.recordCount > 0 ? qName.Name : "Menu " & menuID;
response.steps.append("Using provided menu: " & menuName & " (ID: " & menuID & ")");
} else {
// Create a Menu record for this business (or get existing menu with same name)
menuName = structKeyExists(wizardData, "menuName") && isSimpleValue(wizardData.menuName) && len(trim(wizardData.menuName))
? trim(wizardData.menuName)
: "Main Menu";
// Get menu time range (optional)
menuStartTime = structKeyExists(wizardData, "menuStartTime") && isSimpleValue(wizardData.menuStartTime) && len(trim(wizardData.menuStartTime))
? trim(wizardData.menuStartTime)
: "";
menuEndTime = structKeyExists(wizardData, "menuEndTime") && isSimpleValue(wizardData.menuEndTime) && len(trim(wizardData.menuEndTime))
? trim(wizardData.menuEndTime)
: "";
// Convert HH:MM to HH:MM:SS if needed
if (len(menuStartTime) == 5) menuStartTime = menuStartTime & ":00";
if (len(menuEndTime) == 5) menuEndTime = menuEndTime & ":00";
// Validate menu hours fall within business operating hours
if (len(menuStartTime) && len(menuEndTime)) {
qHours = queryExecute("
SELECT MIN(OpenTime) as earliestOpen, MAX(ClosingTime) as latestClose
FROM Hours
WHERE BusinessID = :bizID
", { bizID: businessId }, { datasource: "payfrit" });
if (qHours.recordCount > 0 && !isNull(qHours.earliestOpen) && !isNull(qHours.latestClose)) {
earliestOpen = timeFormat(qHours.earliestOpen, "HH:mm:ss");
latestClose = timeFormat(qHours.latestClose, "HH:mm:ss");
if (menuStartTime < earliestOpen || menuEndTime > latestClose) {
throw(message="Menu hours (" & menuStartTime & " - " & menuEndTime & ") must be within business operating hours (" & earliestOpen & " - " & latestClose & ")");
}
response.steps.append("Validated menu hours against business hours (" & earliestOpen & " - " & latestClose & ")");
}
}
qMenu = queryExecute("
SELECT ID FROM Menus
WHERE BusinessID = :bizID AND Name = :name AND IsActive = 1
", { bizID: businessId, name: menuName }, { datasource: "payfrit" });
if (qMenu.recordCount > 0) {
menuID = qMenu.ID;
// Update existing menu with new time range if provided
if (len(menuStartTime) && len(menuEndTime)) {
queryExecute("
UPDATE Menus SET StartTime = :startTime, EndTime = :endTime
WHERE ID = :menuID
", {
menuID: menuID,
startTime: menuStartTime,
endTime: menuEndTime
}, { datasource: "payfrit" });
response.steps.append("Updated existing menu: " & menuName & " (ID: " & menuID & ") with hours " & menuStartTime & " - " & menuEndTime);
} else {
response.steps.append("Using existing menu: " & menuName & " (ID: " & menuID & ")");
}
} else {
queryExecute("
INSERT INTO Menus (
BusinessID, Name, DaysActive, StartTime, EndTime, SortOrder, IsActive, AddedOn
) VALUES (
:bizID, :name, 127, :startTime, :endTime, 0, 1, NOW()
)
", {
bizID: businessId,
name: menuName,
startTime: len(menuStartTime) ? menuStartTime : javaCast("null", ""),
endTime: len(menuEndTime) ? menuEndTime : javaCast("null", "")
}, { datasource: "payfrit" });
qNewMenu = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
menuID = qNewMenu.id;
timeInfo = len(menuStartTime) && len(menuEndTime) ? " (" & menuStartTime & " - " & menuEndTime & ")" : " (all day)";
response.steps.append("Created menu: " & menuName & timeInfo & " (ID: " & menuID & ")");
}
}
// Build category map
categories = structKeyExists(wizardData, "categories") ? wizardData.categories : [];
categoryMap = {}; // Maps category name to CategoryID
response.steps.append("Processing " & arrayLen(categories) & " categories...");
catOrder = 1;
for (c = 1; c <= arrayLen(categories); c++) {
cat = categories[c];
catName = structKeyExists(cat, "name") && isSimpleValue(cat.name) ? cat.name : "";
if (!len(catName)) {
response.steps.append("Warning: Skipping category with no name at index " & c);
continue;
}
// Check if category exists in Categories table for this menu
qCat = queryExecute("
SELECT ID FROM Categories
WHERE BusinessID = :bizID AND Name = :name AND MenuID = :menuID
", { bizID: businessId, name: catName, menuID: menuID }, { datasource: "payfrit" });
if (qCat.recordCount > 0) {
categoryID = qCat.CategoryID;
response.steps.append("Category exists: " & catName & " (ID: " & categoryID & ")");
} else {
// Create category in Categories table with MenuID
queryExecute("
INSERT INTO Categories (
BusinessID, MenuID, Name, SortOrder
) VALUES (
:bizID, :menuID, :name, :sortOrder
)
", {
bizID: businessId,
menuID: menuID,
name: catName,
sortOrder: catOrder
}, { datasource: "payfrit" });
qNewCat = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
categoryID = qNewCat.id;
response.steps.append("Created category: " & catName & " in menu " & menuName & " (ID: " & categoryID & ")");
}
categoryMap[catName] = categoryID;
catOrder++;
}
// Create menu items
items = structKeyExists(wizardData, "items") ? wizardData.items : [];
response.steps.append("Processing " & arrayLen(items) & " menu items...");
totalItems = 0;
totalLinks = 0;
// Track item order within each category
categoryItemOrder = {};
for (n = 1; n <= arrayLen(items); n++) {
item = items[n];
if (!isStruct(item)) continue;
// Safely extract all item fields - ensure they're simple values
itemName = structKeyExists(item, "name") && isSimpleValue(item.name) ? item.name : "";
if (!len(itemName)) {
response.steps.append("Warning: Skipping item with no name at index " & n);
continue;
}
itemDesc = "";
if (structKeyExists(item, "description") && isSimpleValue(item.description)) {
itemDesc = item.description;
}
itemPrice = 0;
if (structKeyExists(item, "price")) {
if (isSimpleValue(item.price)) {
itemPrice = val(item.price);
}
}
itemCategory = "";
if (structKeyExists(item, "category") && isSimpleValue(item.category)) {
itemCategory = item.category;
}
itemModifiers = structKeyExists(item, "modifiers") && isArray(item.modifiers) ? item.modifiers : [];
// Get category ID
if (!len(itemCategory) || !structKeyExists(categoryMap, itemCategory)) {
response.steps.append("Warning: Item '" & itemName & "' has unknown category - skipping");
continue;
}
categoryID = categoryMap[itemCategory];
// Track sort order within category
if (!structKeyExists(categoryItemOrder, itemCategory)) {
categoryItemOrder[itemCategory] = 1;
}
itemOrder = categoryItemOrder[itemCategory];
categoryItemOrder[itemCategory]++;
// Check if item exists
qItem = queryExecute("
SELECT ID FROM Items
WHERE BusinessID = :bizID
AND Name = :name
AND CategoryID = :catID
", { bizID: businessId, name: itemName, catID: categoryID }, { datasource: "payfrit" });
if (qItem.recordCount > 0) {
menuItemID = qItem.ID;
// Update existing item
queryExecute("
UPDATE Items SET
Description = :desc,
Price = :price,
SortOrder = :sortOrder
WHERE ItemID = :id
", {
desc: itemDesc,
price: itemPrice,
sortOrder: itemOrder,
id: menuItemID
}, { datasource: "payfrit" });
} else {
queryExecute("
INSERT INTO Items (
BusinessID, Name, Description, ParentItemID,
CategoryID, Price, IsActive, SortOrder
) VALUES (
:bizID, :name, :desc, 0, :catID, :price, 1, :sortOrder
)
", {
bizID: businessId,
name: itemName,
desc: itemDesc,
catID: categoryID,
price: itemPrice,
sortOrder: itemOrder
}, { datasource: "payfrit" });
qNewItem = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
menuItemID = qNewItem.id;
}
totalItems++;
// Link modifier templates to this item
modOrder = 1;
for (m = 1; m <= arrayLen(itemModifiers); m++) {
modRef = itemModifiers[m];
// Handle both string modifier names and struct references
if (isSimpleValue(modRef)) {
modName = modRef;
} else if (isStruct(modRef) && structKeyExists(modRef, "name")) {
modName = modRef.name;
} else {
continue; // Skip invalid modifier reference
}
if (structKeyExists(templateMap, modName)) {
templateItemID = templateMap[modName];
// Check if link exists
qLink = queryExecute("
SELECT 1 FROM lt_ItemID_TemplateItemID
WHERE ItemID = :itemID AND TemplateItemID = :templateID
", { itemID: menuItemID, templateID: templateItemID }, { datasource: "payfrit" });
if (qLink.recordCount == 0) {
queryExecute("
INSERT INTO lt_ItemID_TemplateItemID (ItemID, TemplateItemID, SortOrder)
VALUES (:itemID, :templateID, :sortOrder)
", {
itemID: menuItemID,
templateID: templateItemID,
sortOrder: modOrder
}, { datasource: "payfrit" });
totalLinks++;
}
modOrder++;
}
}
}
response.steps.append("Created/updated " & totalItems & " items with " & totalLinks & " modifier links");
response.OK = true;
response.summary = {
"businessId": businessId,
"categoriesProcessed": arrayLen(categories),
"templatesProcessed": arrayLen(modTemplates),
"itemsProcessed": totalItems,
"linksCreated": totalLinks
};
} catch (any e) {
response.errors.append(e.message);
if (len(e.detail)) {
response.errors.append(e.detail);
}
}
writeOutput(serializeJSON(response));
</cfscript>