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 30dd0997b9 Seed order-fulfillment task types on business creation
Add Deliver to Table, Order Ready for Pickup, and Deliver to Address
to the default task types created by the setup wizard. These are
required by updateStatus.cfm to auto-create tasks at status 3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:13:08 -08:00

823 lines
34 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": [] };
itemsDir = expandPath("/uploads/items");
// Helper function: Resize image maintaining aspect ratio, fitting within maxSize box
function resizeToFit(img, maxSize) {
w = imageGetWidth(img);
h = imageGetHeight(img);
if (w <= maxSize && h <= maxSize) return img;
if (w > h) {
newW = maxSize;
newH = int(h * (maxSize / w));
} else {
newH = maxSize;
newW = int(w * (maxSize / h));
}
imageResize(img, newW, newH, "highQuality");
return img;
}
// Helper function: Create square thumbnail (center crop)
function createSquareThumb(img, size) {
thumb = imageCopy(img, 0, 0, imageGetWidth(img), imageGetHeight(img));
w = imageGetWidth(thumb);
h = imageGetHeight(thumb);
// Resize so smallest dimension equals size
if (w > h) {
imageResize(thumb, "", size, "highQuality");
} else {
imageResize(thumb, size, "", "highQuality");
}
// Center crop to square
w = imageGetWidth(thumb);
h = imageGetHeight(thumb);
if (w > h) {
x = int((w - h) / 2);
imageCrop(thumb, x, 0, h, h);
} else if (h > w) {
y = int((h - w) / 2);
imageCrop(thumb, 0, y, w, w);
}
// Final resize to exact size
imageResize(thumb, size, size, "highQuality");
return thumb;
}
// Helper function: Download and save item image from URL
function downloadItemImage(itemID, imageUrl) {
try {
// Skip empty or invalid URLs
if (!len(trim(imageUrl))) return false;
// Make URL absolute if relative
if (left(imageUrl, 2) == "//") {
imageUrl = "https:" & imageUrl;
} else if (left(imageUrl, 1) == "/") {
// Can't resolve relative URL without base - skip
return false;
}
// Download the image
http url="#imageUrl#" method="GET" timeout="30" result="httpResult" {
httpparam type="header" name="User-Agent" value="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
}
if (httpResult.statusCode != "200 OK" && !httpResult.statusCode.startsWith("200")) {
return false;
}
// Check if it's actually an image
contentType = structKeyExists(httpResult.responseHeader, "Content-Type") ? httpResult.responseHeader["Content-Type"] : "";
if (!findNoCase("image", contentType)) {
return false;
}
// Write to temp file
tempFile = itemsDir & "/temp_" & itemID & "_" & createUUID() & ".tmp";
fileWrite(tempFile, httpResult.fileContent);
// Read as image
img = imageNew(tempFile);
// Create thumbnail (128x128 square for retina)
thumb = createSquareThumb(img, 128);
thumbPath = itemsDir & "/" & itemID & "_thumb.jpg";
imageWrite(thumb, thumbPath, 0.85);
// Create medium size (400px max)
medium = imageCopy(img, 0, 0, imageGetWidth(img), imageGetHeight(img));
medium = resizeToFit(medium, 400);
mediumPath = itemsDir & "/" & itemID & "_medium.jpg";
imageWrite(medium, mediumPath, 0.85);
// Save full size (max 1200px)
img = resizeToFit(img, 1200);
fullPath = itemsDir & "/" & itemID & ".jpg";
imageWrite(img, fullPath, 0.90);
// Clean up temp file
try { fileDelete(tempFile); } catch (any e) {}
return true;
} catch (any e) {
// Clean up temp file if it exists
if (isDefined("tempFile") && fileExists(tempFile)) {
try { fileDelete(tempFile); } catch (any e2) {}
}
return false;
}
}
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;
// Extract brand color (6-digit hex without #)
bizBrandColor = structKeyExists(biz, "brandColor") && isSimpleValue(biz.brandColor) ? trim(biz.brandColor) : "";
// Ensure it's a valid 6-digit hex (remove # if present)
bizBrandColor = reReplace(bizBrandColor, "^##?", "");
if (!reFind("^[0-9A-Fa-f]{6}$", bizBrandColor)) {
bizBrandColor = ""; // Invalid format, skip it
}
// 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 = queryTimed("
SELECT ID 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.ID;
response.steps.append("Using stateID: " & stateID);
}
}
queryTimed("
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 = queryTimed("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
queryTimed("
INSERT INTO Businesses (Name, Phone, UserID, AddressID, DeliveryZIPCodes, CommunityMealType, TaxRate, BrandColor, AddedOn)
VALUES (:name, :phone, :userId, :addressId, :deliveryZips, :communityMealType, :taxRate, :brandColor, NOW())
", {
name: bizName,
phone: bizPhone,
userId: userId,
addressId: addressId,
deliveryZips: len(zip) ? zip : "",
communityMealType: communityMealType,
taxRate: { value: bizTaxRate, cfsqltype: "cf_sql_decimal" },
brandColor: { value: len(bizBrandColor) ? bizBrandColor : javaCast("null", ""), cfsqltype: "cf_sql_varchar", null: !len(bizBrandColor) }
}, { datasource: "payfrit" });
qNewBiz = queryTimed("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
queryTimed("
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 Staff (notifications icon, purple)
// 2. Chat With Staff (chat icon, blue)
// 3. Pay With Cash (payments icon, green)
defaultTaskTypes = [
{ name: "Call Staff", icon: "notifications", color: "##9C27B0", description: "Request staff 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" },
{ name: "Deliver to Table", icon: "restaurant", color: "##FF9800", description: "Deliver completed order to table" },
{ name: "Order Ready for Pickup", icon: "shopping_bag", color: "##00BCD4", description: "Notify customer their order is ready" },
{ name: "Deliver to Address", icon: "local_shipping", color: "##795548", description: "Deliver order to customer address" }
];
for (tt = 1; tt <= arrayLen(defaultTaskTypes); tt++) {
taskType = defaultTaskTypes[tt];
queryTimed("
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 6 default task types");
// Create default task categories for the business
defaultTaskCategories = [
{ name: "Service Point", color: "##F44336" }, // Red
{ name: "Kitchen", color: "##FF9800" }, // Orange
{ name: "Bar", color: "##9C27B0" }, // Purple
{ name: "Cleaning", color: "##4CAF50" }, // Green
{ name: "Management", color: "##2196F3" }, // Blue
{ name: "Delivery", color: "##00BCD4" }, // Cyan
{ name: "General", color: "##607D8B" } // Blue Grey
];
for (tc = 1; tc <= arrayLen(defaultTaskCategories); tc++) {
taskCat = defaultTaskCategories[tc];
queryTimed("
INSERT INTO TaskCategories (BusinessID, Name, Color)
VALUES (:businessID, :name, :color)
", {
businessID: { value: businessId, cfsqltype: "cf_sql_integer" },
name: { value: taskCat.name, cfsqltype: "cf_sql_varchar" },
color: { value: taskCat.color, cfsqltype: "cf_sql_varchar" }
}, { datasource: "payfrit" });
}
response.steps.append("Created 7 default task categories");
// 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
queryTimed("
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 = queryTimed("
SELECT ID, Name AS BusinessName 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.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 = queryTimed("
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.ID;
response.steps.append("Template exists: " & tmplName & " (ID: " & templateItemID & ")");
} else {
// Create template as Item with CategoryID=0 to mark as template
queryTimed("
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 = queryTimed("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 = queryTimed("
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) {
queryTimed("
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 = queryTimed("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 = queryTimed("
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 = queryTimed("
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)) {
queryTimed("
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 {
queryTimed("
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 = queryTimed("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...");
// First pass: create top-level categories (no parentCategoryName)
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;
}
// Skip subcategories in first pass
if (structKeyExists(cat, "parentCategoryName") && len(trim(cat.parentCategoryName))) continue;
// Check if category exists in Categories table for this menu
qCat = queryTimed("
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.ID;
response.steps.append("Category exists: " & catName & " (ID: " & categoryID & ")");
} else {
queryTimed("
INSERT INTO Categories (
BusinessID, MenuID, Name, SortOrder
) VALUES (
:bizID, :menuID, :name, :sortOrder
)
", {
bizID: businessId,
menuID: menuID,
name: catName,
sortOrder: catOrder
}, { datasource: "payfrit" });
qNewCat = queryTimed("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++;
}
// Second pass: create subcategories (have parentCategoryName)
for (c = 1; c <= arrayLen(categories); c++) {
cat = categories[c];
catName = structKeyExists(cat, "name") && isSimpleValue(cat.name) ? cat.name : "";
if (!len(catName)) continue;
parentName = structKeyExists(cat, "parentCategoryName") ? trim(cat.parentCategoryName) : "";
if (!len(parentName)) continue;
parentCatID = structKeyExists(categoryMap, parentName) ? categoryMap[parentName] : 0;
if (parentCatID == 0) {
response.steps.append("Warning: Parent category '" & parentName & "' not found for subcategory '" & catName & "'");
continue;
}
qCat = queryTimed("
SELECT ID FROM Categories
WHERE BusinessID = :bizID AND Name = :name AND MenuID = :menuID AND ParentCategoryID = :parentCatID
", { bizID: businessId, name: catName, menuID: menuID, parentCatID: parentCatID }, { datasource: "payfrit" });
if (qCat.recordCount > 0) {
categoryID = qCat.ID;
response.steps.append("Subcategory exists: " & catName & " under " & parentName & " (ID: " & categoryID & ")");
} else {
queryTimed("
INSERT INTO Categories (
BusinessID, MenuID, Name, ParentCategoryID, SortOrder
) VALUES (
:bizID, :menuID, :name, :parentCatID, :sortOrder
)
", {
bizID: businessId,
menuID: menuID,
name: catName,
parentCatID: parentCatID,
sortOrder: catOrder
}, { datasource: "payfrit" });
qNewCat = queryTimed("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
categoryID = qNewCat.id;
response.steps.append("Created subcategory: " & catName & " under " & parentName & " (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;
totalImages = 0;
// Track item order within each category
categoryItemOrder = {};
// Track item name -> database ID mapping for image uploads
itemIdMap = {};
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 = queryTimed("
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
queryTimed("
UPDATE Items SET
Description = :desc,
Price = :price,
SortOrder = :sortOrder
WHERE ID = :id
", {
desc: itemDesc,
price: itemPrice,
sortOrder: itemOrder,
id: menuItemID
}, { datasource: "payfrit" });
} else {
queryTimed("
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 = queryTimed("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
menuItemID = qNewItem.id;
}
totalItems++;
// Track mapping for image uploads (use frontend ID if present, else name)
frontendId = structKeyExists(item, "id") && isSimpleValue(item.id) ? item.id : "";
if (len(frontendId)) {
itemIdMap[frontendId] = menuItemID;
}
itemIdMap[itemName] = menuItemID;
// 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 = queryTimed("
SELECT 1 FROM lt_ItemID_TemplateItemID
WHERE ItemID = :itemID AND TemplateItemID = :templateID
", { itemID: menuItemID, templateID: templateItemID }, { datasource: "payfrit" });
if (qLink.recordCount == 0) {
queryTimed("
INSERT INTO lt_ItemID_TemplateItemID (ItemID, TemplateItemID, SortOrder)
VALUES (:itemID, :templateID, :sortOrder)
", {
itemID: menuItemID,
templateID: templateItemID,
sortOrder: modOrder
}, { datasource: "payfrit" });
totalLinks++;
}
modOrder++;
}
}
// Download item image if URL provided
itemImageUrl = "";
if (structKeyExists(item, "imageUrl") && isSimpleValue(item.imageUrl)) {
itemImageUrl = trim(item.imageUrl);
}
if (len(itemImageUrl)) {
if (downloadItemImage(menuItemID, itemImageUrl)) {
totalImages++;
}
}
}
response.steps.append("Created/updated " & totalItems & " items with " & totalLinks & " modifier links" & (totalImages > 0 ? " and " & totalImages & " images downloaded" : ""));
response.OK = true;
response.summary = {
"businessId": businessId,
"categoriesProcessed": arrayLen(categories),
"templatesProcessed": arrayLen(modTemplates),
"itemsProcessed": totalItems,
"linksCreated": totalLinks,
"imagesDownloaded": totalImages,
"itemIdMap": itemIdMap
};
// Clean up temp folder from ZIP upload if provided
tempFolder = structKeyExists(data, "tempFolder") && isSimpleValue(data.tempFolder) ? trim(data.tempFolder) : "";
if (len(tempFolder)) {
// Validate folder name is safe (alphanumeric only - UUID without dashes)
if (reFind("^[a-f0-9]{32}$", tempFolder)) {
tempFolderPath = expandPath("/temp/menu-import/" & tempFolder);
if (directoryExists(tempFolderPath)) {
try {
directoryDelete(tempFolderPath, true);
response.steps.append("Cleaned up temp folder: " & tempFolder);
} catch (any cleanupErr) {
response.steps.append("Warning: Could not delete temp folder: " & cleanupErr.message);
}
}
}
}
} catch (any e) {
response.errors.append(e.message);
if (len(e.detail)) {
response.errors.append(e.detail);
}
}
writeOutput(serializeJSON(response));
</cfscript>