payfrit-works/api/setup/saveWizard.cfm
John Mizerek d4e0ae1162 Add branding features: header upload and brand color picker
- Add uploadHeader.cfm API for 1200px header images
- Add saveBrandColor.cfm API for hex color storage
- Add Branding section to menu builder sidebar
- Fix header upload path and permissions
- Various beacon and service point API improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 12:14:24 -08:00

463 lines
18 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;
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) : "";
// 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: stateID,
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 & ")");
// Create new business with address link and phone
queryExecute("
INSERT INTO Businesses (BusinessName, BusinessPhone, BusinessUserID, BusinessAddressID, BusinessDeliveryZipCodes, BusinessAddedOn)
VALUES (:name, :phone, :userId, :addressId, :deliveryZips, NOW())
", {
name: bizName,
phone: bizPhone,
userId: userId,
addressId: addressId,
deliveryZips: len(zip) ? zip : ""
}, { 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");
// 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++;
}
}
// 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
qCat = queryExecute("
SELECT CategoryID FROM Categories
WHERE CategoryBusinessID = :bizID AND CategoryName = :name
", { bizID: businessId, name: catName }, { datasource: "payfrit" });
if (qCat.recordCount > 0) {
categoryID = qCat.CategoryID;
response.steps.append("Category exists: " & catName & " (ID: " & categoryID & ")");
} else {
// Create category in Categories table
queryExecute("
INSERT INTO Categories (
CategoryBusinessID, CategoryName, CategorySortOrder
) VALUES (
:bizID, :name, :sortOrder
)
", {
bizID: businessId,
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 & " (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>