payfrit-works/api/setup/importBusiness.cfm
John Mizerek 51a80b537d Add local dev support and fix menu builder API
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>
2026-01-04 22:47:12 -08:00

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>