Portal local development: - Add BASE_PATH detection to all portal files (login, portal.js, menu-builder, station-assignment) - Allows portal to work at /biz.payfrit.com/ path locally Menu Builder fixes: - Fix duplicate template options in getForBuilder.cfm query - Filter template children by business ID with DISTINCT New APIs: - api/portal/myBusinesses.cfm - List businesses for logged-in user - api/stations/list.cfm - List KDS stations - api/menu/updateStations.cfm - Update item station assignments - api/setup/reimportBigDeans.cfm - Full Big Dean's menu import script Admin utilities: - Various debug and migration scripts for menu/template management - Beacon switching, category cleanup, modifier template setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
366 lines
14 KiB
Text
366 lines
14 KiB
Text
<cfsetting showdebugoutput="false">
|
|
<cfsetting enablecfoutputonly="true">
|
|
<cfsetting requesttimeout="300">
|
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
|
|
|
<cfscript>
|
|
/**
|
|
* Import Business from Scraped Data
|
|
*
|
|
* Creates a complete business with:
|
|
* - Business record with contact info
|
|
* - Categories as Items (ParentID=0)
|
|
* - Menu items as children of categories
|
|
* - Modifier templates linked via ItemTemplateLinks
|
|
* - Modifier options as children of templates
|
|
*
|
|
* POST JSON structure:
|
|
* {
|
|
* "business": {
|
|
* "name": "Big Dean's Ocean Front Cafe",
|
|
* "address": "1615 Ocean Front Walk",
|
|
* "city": "Santa Monica",
|
|
* "state": "CA",
|
|
* "zip": "90401",
|
|
* "phone": "(310) 393-2666",
|
|
* "email": "owner@bigdeans.com",
|
|
* "ownerPhone": "3101234567",
|
|
* "website": "https://bigdeansoceanfrontcafe.com",
|
|
* "logoUrl": "https://...",
|
|
* "headerUrl": "https://...",
|
|
* "hours": {
|
|
* "mon": "12:00 PM - 7:00 PM",
|
|
* "tue": "11:00 AM - 8:00 PM",
|
|
* ...
|
|
* }
|
|
* },
|
|
* "categories": [
|
|
* {
|
|
* "name": "World Famous Burgers",
|
|
* "sortOrder": 1,
|
|
* "items": [
|
|
* {
|
|
* "name": "Big Dean's Burger",
|
|
* "description": "The burger that made Santa Monica famous!",
|
|
* "price": 12.99,
|
|
* "modifiers": ["onion_style", "add_cheese"] // template references
|
|
* }
|
|
* ]
|
|
* }
|
|
* ],
|
|
* "modifierTemplates": [
|
|
* {
|
|
* "id": "onion_style",
|
|
* "name": "Onions",
|
|
* "required": true,
|
|
* "maxSelections": 1,
|
|
* "options": [
|
|
* { "name": "No Onions", "price": 0, "isDefault": false },
|
|
* { "name": "Grilled Onions", "price": 0, "isDefault": true },
|
|
* { "name": "Raw Onions", "price": 0, "isDefault": false }
|
|
* ]
|
|
* }
|
|
* ],
|
|
* "ownerUserID": 2, // optional, defaults to 1
|
|
* "dryRun": false // optional, if true just validates without inserting
|
|
* }
|
|
*/
|
|
|
|
response = { "OK": false, "steps": [], "errors": [], "warnings": [] };
|
|
|
|
try {
|
|
requestBody = toString(getHttpRequestData().content);
|
|
if (!len(requestBody)) {
|
|
throw(message="No request body provided");
|
|
}
|
|
|
|
data = deserializeJSON(requestBody);
|
|
dryRun = structKeyExists(data, "dryRun") && data.dryRun == true;
|
|
|
|
if (dryRun) {
|
|
response.steps.append("DRY RUN MODE - no changes will be made");
|
|
}
|
|
|
|
// Validate required fields
|
|
if (!structKeyExists(data, "business") || !structKeyExists(data.business, "name")) {
|
|
throw(message="business.name is required");
|
|
}
|
|
|
|
biz = data.business;
|
|
ownerUserID = structKeyExists(data, "ownerUserID") ? val(data.ownerUserID) : 1;
|
|
|
|
// Step 1: Create or find business
|
|
response.steps.append("Step 1: Creating business record...");
|
|
|
|
qCheck = queryExecute("
|
|
SELECT BusinessID FROM Businesses WHERE BusinessName = :name
|
|
", { name: biz.name }, { datasource: "payfrit" });
|
|
|
|
if (qCheck.recordCount > 0) {
|
|
BusinessID = qCheck.BusinessID;
|
|
response.steps.append("Business already exists with ID: " & BusinessID);
|
|
response.warnings.append("Existing business found - will add to existing menu");
|
|
} else if (!dryRun) {
|
|
queryExecute("
|
|
INSERT INTO Businesses (BusinessName, BusinessUserID, BusinessAddressID, BusinessDeliveryZipCodes, BusinessAddedOn) VALUES (:name, :ownerID, 0, '', NOW())
|
|
", {
|
|
name: biz.name, ownerID: ownerUserID
|
|
}, { datasource: "payfrit" });
|
|
|
|
qNew = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
|
BusinessID = qNew.id;
|
|
response.steps.append("Created business with ID: " & BusinessID);
|
|
} else {
|
|
BusinessID = 0;
|
|
response.steps.append("Would create business: " & biz.name);
|
|
}
|
|
|
|
// Step 2: Create modifier templates first (they need to exist before items reference them)
|
|
response.steps.append("Step 2: Creating modifier templates...");
|
|
|
|
templateMap = {}; // Maps template string ID to database ItemID
|
|
templates = structKeyExists(data, "modifierTemplates") ? data.modifierTemplates : [];
|
|
|
|
for (tmpl in templates) {
|
|
templateStringID = tmpl.id;
|
|
templateName = tmpl.name;
|
|
required = structKeyExists(tmpl, "required") && tmpl.required == true;
|
|
maxSelections = structKeyExists(tmpl, "maxSelections") ? val(tmpl.maxSelections) : 0;
|
|
|
|
if (!dryRun) {
|
|
// Check if template already exists for this business
|
|
qTmpl = queryExecute("
|
|
SELECT ItemID FROM Items
|
|
WHERE ItemBusinessID = :bizID
|
|
AND ItemName = :name
|
|
AND ItemParentItemID = 0
|
|
AND ItemID IN (SELECT TemplateItemID FROM ItemTemplateLinks)
|
|
", { bizID: BusinessID, name: templateName }, { datasource: "payfrit" });
|
|
|
|
if (qTmpl.recordCount > 0) {
|
|
templateItemID = qTmpl.ItemID;
|
|
response.steps.append("Template exists: " & templateName & " (ID: " & templateItemID & ")");
|
|
} else {
|
|
// Create template as Item at ParentID=0, with ItemIsCollapsible=1 to mark it as a template
|
|
// This ensures the API filter excludes it from categories
|
|
queryExecute("
|
|
INSERT INTO Items (
|
|
ItemBusinessID, ItemName, ItemParentItemID, ItemPrice,
|
|
ItemIsActive, ItemRequiresChildSelection, ItemMaxNumSelectionReq, ItemSortOrder, ItemIsCollapsible
|
|
) VALUES (
|
|
:bizID, :name, 0, 0, 1, :required, :maxSelect, 0, 1
|
|
)
|
|
", {
|
|
bizID: BusinessID,
|
|
name: templateName,
|
|
required: required ? 1 : 0,
|
|
maxSelect: maxSelections
|
|
}, { datasource: "payfrit" });
|
|
|
|
qNewTmpl = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
|
templateItemID = qNewTmpl.id;
|
|
response.steps.append("Created template: " & templateName & " (ID: " & templateItemID & ")");
|
|
}
|
|
|
|
templateMap[templateStringID] = templateItemID;
|
|
|
|
// Create template options
|
|
options = structKeyExists(tmpl, "options") ? tmpl.options : [];
|
|
optionOrder = 1;
|
|
for (opt in options) {
|
|
optName = opt.name;
|
|
optPrice = structKeyExists(opt, "price") ? val(opt.price) : 0;
|
|
optDefault = structKeyExists(opt, "isDefault") && opt.isDefault == true;
|
|
|
|
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, ItemPrice,
|
|
ItemIsActive, ItemIsCheckedByDefault, ItemSortOrder
|
|
) VALUES (
|
|
:bizID, :name, :parentID, :price, 1, :isDefault, :sortOrder
|
|
)
|
|
", {
|
|
bizID: BusinessID,
|
|
name: optName,
|
|
parentID: templateItemID,
|
|
price: optPrice,
|
|
isDefault: optDefault ? 1 : 0,
|
|
sortOrder: optionOrder
|
|
}, { datasource: "payfrit" });
|
|
}
|
|
optionOrder++;
|
|
}
|
|
} else {
|
|
response.steps.append("Would create template: " & templateName & " with " & arrayLen(tmpl.options) & " options");
|
|
templateMap[templateStringID] = 0;
|
|
}
|
|
}
|
|
|
|
// Step 3: Create categories (as Items at ParentID=0)
|
|
response.steps.append("Step 3: Creating categories...");
|
|
|
|
categoryMap = {}; // Maps category name to ItemID
|
|
categories = structKeyExists(data, "categories") ? data.categories : [];
|
|
catOrder = 1;
|
|
|
|
for (cat in categories) {
|
|
catName = cat.name;
|
|
|
|
if (!dryRun) {
|
|
// Check if category exists (Item at ParentID=0, not a template)
|
|
qCat = queryExecute("
|
|
SELECT ItemID FROM Items
|
|
WHERE ItemBusinessID = :bizID
|
|
AND ItemName = :name
|
|
AND ItemParentItemID = 0
|
|
AND ItemID NOT IN (SELECT TemplateItemID FROM ItemTemplateLinks)
|
|
", { bizID: BusinessID, name: catName }, { datasource: "payfrit" });
|
|
|
|
if (qCat.recordCount > 0) {
|
|
categoryItemID = qCat.ItemID;
|
|
response.steps.append("Category exists: " & catName);
|
|
} else {
|
|
queryExecute("
|
|
INSERT INTO Items (
|
|
ItemBusinessID, ItemName, ItemParentItemID, ItemPrice,
|
|
ItemIsActive, ItemSortOrder
|
|
) VALUES (
|
|
:bizID, :name, 0, 0, 1, :sortOrder
|
|
)
|
|
", {
|
|
bizID: BusinessID,
|
|
name: catName,
|
|
sortOrder: catOrder
|
|
}, { datasource: "payfrit" });
|
|
|
|
qNewCat = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
|
categoryItemID = qNewCat.id;
|
|
response.steps.append("Created category: " & catName & " (ID: " & categoryItemID & ")");
|
|
}
|
|
|
|
categoryMap[catName] = categoryItemID;
|
|
} else {
|
|
response.steps.append("Would create category: " & catName);
|
|
categoryMap[catName] = 0;
|
|
}
|
|
catOrder++;
|
|
}
|
|
|
|
// Step 4: Create menu items as children of categories
|
|
response.steps.append("Step 4: Creating menu items...");
|
|
|
|
totalItems = 0;
|
|
totalLinks = 0;
|
|
|
|
for (cat in categories) {
|
|
catName = cat.name;
|
|
categoryItemID = categoryMap[catName];
|
|
items = structKeyExists(cat, "items") ? cat.items : [];
|
|
itemOrder = 1;
|
|
|
|
for (item in items) {
|
|
itemName = item.name;
|
|
itemDesc = structKeyExists(item, "description") ? item.description : "";
|
|
itemPrice = structKeyExists(item, "price") ? val(item.price) : 0;
|
|
|
|
if (!dryRun) {
|
|
// Check if item exists
|
|
qItem = queryExecute("
|
|
SELECT ItemID FROM Items
|
|
WHERE ItemBusinessID = :bizID AND ItemName = :name AND ItemParentItemID = :parentID
|
|
", { bizID: BusinessID, name: itemName, parentID: categoryItemID }, { datasource: "payfrit" });
|
|
|
|
if (qItem.recordCount > 0) {
|
|
menuItemID = qItem.ItemID;
|
|
} else {
|
|
queryExecute("
|
|
INSERT INTO Items (
|
|
ItemBusinessID, ItemName, ItemDescription, ItemParentItemID,
|
|
ItemPrice, ItemIsActive, ItemSortOrder
|
|
) VALUES (
|
|
:bizID, :name, :desc, :parentID, :price, 1, :sortOrder
|
|
)
|
|
", {
|
|
bizID: BusinessID,
|
|
name: itemName,
|
|
desc: itemDesc,
|
|
parentID: categoryItemID,
|
|
price: itemPrice,
|
|
sortOrder: itemOrder
|
|
}, { datasource: "payfrit" });
|
|
|
|
qNewItem = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
|
menuItemID = qNewItem.id;
|
|
}
|
|
|
|
// Link modifier templates to this item
|
|
modifiers = structKeyExists(item, "modifiers") ? item.modifiers : [];
|
|
modOrder = 1;
|
|
for (modRef in modifiers) {
|
|
if (structKeyExists(templateMap, modRef)) {
|
|
templateItemID = templateMap[modRef];
|
|
|
|
// 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++;
|
|
} else {
|
|
response.warnings.append("Unknown modifier reference: " & modRef & " on item: " & itemName);
|
|
}
|
|
}
|
|
}
|
|
|
|
totalItems++;
|
|
itemOrder++;
|
|
}
|
|
}
|
|
|
|
response.steps.append("Created " & totalItems & " menu items with " & totalLinks & " template links");
|
|
|
|
// Summary
|
|
response.OK = true;
|
|
response.summary = {
|
|
"businessID": BusinessID,
|
|
"businessName": biz.name,
|
|
"categoriesCreated": arrayLen(categories),
|
|
"templatesCreated": arrayLen(templates),
|
|
"itemsCreated": totalItems,
|
|
"templateLinksCreated": totalLinks,
|
|
"dryRun": dryRun
|
|
};
|
|
|
|
if (dryRun) {
|
|
response.steps.append("DRY RUN COMPLETE - no changes were made");
|
|
} else {
|
|
response.steps.append("IMPORT COMPLETE!");
|
|
}
|
|
|
|
} catch (any e) {
|
|
response.errors.append(e.message);
|
|
if (len(e.detail)) {
|
|
response.errors.append(e.detail);
|
|
}
|
|
}
|
|
|
|
writeOutput(serializeJSON(response));
|
|
</cfscript>
|