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 3613776ff3 Fix undefined menuName in saveWizard when using provided menuId
The menuName variable was only defined in the else branch (new menu
creation) but referenced later in category logging. Now looks up the
menu name from DB when using a provided menuId.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 17:54:16 -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 tt_StateAbbreviation = :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 (AddressLine1, AddressCity, AddressStateID, AddressZIPCode, AddressUserID, AddressTypeID, AddressAddedOn)
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 (BusinessName, BusinessPhone, BusinessUserID, BusinessAddressID, BusinessDeliveryZipCodes, BusinessCommunityMealType, BusinessTaxRate, BusinessAddedOn)
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 AddressBusinessID = :businessID WHERE AddressID = :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 (tt_TaskTypeName, tt_TaskTypeDescription, tt_TaskTypeIcon, tt_TaskTypeColor, tt_TaskTypeBusinessID, tt_TaskTypeSortOrder)
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 (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime)
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 BusinessID, BusinessName FROM Businesses WHERE BusinessID = :id
", { id: businessId }, { datasource: "payfrit" });
if (qBiz.recordCount == 0) {
throw(message="Business not found: " & businessId);
}
response.steps.append("Found existing business: " & qBiz.BusinessName);
}
// 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.ItemID FROM Items i
WHERE i.ItemBusinessID = :bizID
AND i.ItemName = :name
AND i.ItemParentItemID = 0
AND i.ItemCategoryID = 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 ItemCategoryID=0 to mark as template
queryExecute("
INSERT INTO Items (
ItemBusinessID, ItemName, ItemParentItemID, ItemCategoryID, ItemPrice,
ItemIsActive, ItemRequiresChildSelection, ItemMaxNumSelectionReq,
ItemSortOrder
) 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 ItemID FROM Items
WHERE ItemBusinessID = :bizID AND ItemName = :name AND ItemParentItemID = :parentID
", { bizID: businessId, name: optName, parentID: templateItemID }, { datasource: "payfrit" });
if (qOpt.recordCount == 0) {
queryExecute("
INSERT INTO Items (
ItemBusinessID, ItemName, ItemParentItemID, ItemCategoryID,
ItemPrice, ItemIsActive, ItemSortOrder
) 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
qMenuName = queryExecute("SELECT MenuName FROM Menus WHERE MenuID = :id", { id: menuID }, { datasource: "payfrit" });
menuName = qMenuName.recordCount > 0 ? qMenuName.MenuName : "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(HoursOpenTime) as earliestOpen, MAX(HoursClosingTime) as latestClose
FROM Hours
WHERE HoursBusinessID = :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 MenuID FROM Menus
WHERE MenuBusinessID = :bizID AND MenuName = :name AND MenuIsActive = 1
", { bizID: businessId, name: menuName }, { datasource: "payfrit" });
if (qMenu.recordCount > 0) {
menuID = qMenu.MenuID;
// Update existing menu with new time range if provided
if (len(menuStartTime) && len(menuEndTime)) {
queryExecute("
UPDATE Menus SET MenuStartTime = :startTime, MenuEndTime = :endTime
WHERE MenuID = :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 (
MenuBusinessID, MenuName, MenuDaysActive, MenuStartTime, MenuEndTime, MenuSortOrder, MenuIsActive, MenuAddedOn
) 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 CategoryID FROM Categories
WHERE CategoryBusinessID = :bizID AND CategoryName = :name AND CategoryMenuID = :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 (
CategoryBusinessID, CategoryMenuID, CategoryName, CategorySortOrder
) 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 ItemID FROM Items
WHERE ItemBusinessID = :bizID
AND ItemName = :name
AND ItemCategoryID = :catID
", { bizID: businessId, name: itemName, catID: categoryID }, { datasource: "payfrit" });
if (qItem.recordCount > 0) {
menuItemID = qItem.ItemID;
// Update existing item
queryExecute("
UPDATE Items SET
ItemDescription = :desc,
ItemPrice = :price,
ItemSortOrder = :sortOrder
WHERE ItemID = :id
", {
desc: itemDesc,
price: itemPrice,
sortOrder: itemOrder,
id: menuItemID
}, { datasource: "payfrit" });
} else {
queryExecute("
INSERT INTO Items (
ItemBusinessID, ItemName, ItemDescription, ItemParentItemID,
ItemCategoryID, ItemPrice, ItemIsActive, ItemSortOrder
) 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 ItemTemplateLinks
WHERE ItemID = :itemID AND TemplateItemID = :templateID
", { itemID: menuItemID, templateID: templateItemID }, { datasource: "payfrit" });
if (qLink.recordCount == 0) {
queryExecute("
INSERT INTO ItemTemplateLinks (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>