payfrit-works/api/menu/saveFromBuilder.cfm
John Mizerek d225133c68 Add menu builder required selection UI and fix portal issues
Menu Builder - Required Selections:
- Added "Selection Rules" section for modifier groups
- Required (Yes/No) dropdown to mark if customer must select an option
- Max Selections input (0 = unlimited) to limit selections
- Visual "Required" badge (red) and "Max X" badge in modifier list
- Updated saveFromBuilder.cfm to persist ItemRequiresChildSelection
  and ItemMaxNumSelectionReq to database

Portal Fixes:
- Fixed menu-builder link to include BASE_PATH for local dev
- Fixed stats.cfm to not reference non-existent Categories table
- Menu items count now uses ItemParentItemID > 0 (not ItemCategoryID)

Stripe Configuration:
- Added api/config/stripe.cfm for centralized Stripe key management
- Supports test/live mode switching
- Fee configuration variables (5% customer, 5% business, 2.9% + $0.30 card)

Payment Intent API:
- Updated createPaymentIntent.cfm with proper fee structure
- Customer pays: subtotal + tax + tip + 5% Payfrit fee + card processing
- Platform receives 10% total (5% from customer + 5% from business)
- Saves fee breakdown to order record

Beacon Management:
- Updated switchBeacons.cfm to move beacons between businesses
- Currently configured: Big Dean's (27) -> In-N-Out (17)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

305 lines
13 KiB
Text

<cfscript>
// Save menu data from the builder UI
// Input: BusinessID, Menu (JSON structure)
// Output: { OK: true }
//
// Supports both old schema (Categories table) and new unified schema (Categories as Items)
response = { "OK": false };
try {
requestBody = toString(getHttpRequestData().content);
if (!len(requestBody)) {
throw("Request body is required");
}
jsonData = deserializeJSON(requestBody);
businessID = val(jsonData.BusinessID ?: 0);
menu = jsonData.Menu ?: {};
if (businessID == 0) {
throw("BusinessID is required");
}
if (!structKeyExists(menu, "categories") || !isArray(menu.categories)) {
throw("Menu categories are required");
}
// Check if new schema is active (ItemBusinessID column exists and has data)
newSchemaActive = false;
try {
qCheck = queryExecute("
SELECT COUNT(*) as cnt FROM Items
WHERE ItemBusinessID = :businessID AND ItemBusinessID > 0
", { businessID: businessID });
newSchemaActive = (qCheck.cnt > 0);
} catch (any e) {
newSchemaActive = false;
}
// Process each category
for (cat in menu.categories) {
categoryID = 0;
categoryDbId = structKeyExists(cat, "dbId") ? val(cat.dbId) : 0;
if (newSchemaActive) {
// NEW SCHEMA: Categories are Items with ParentID=0 and Template=0
if (categoryDbId > 0) {
categoryID = categoryDbId;
// Update existing category Item
queryExecute("
UPDATE Items
SET ItemName = :name,
ItemSortOrder = :sortOrder
WHERE ItemID = :categoryID
AND ItemBusinessID = :businessID
", {
categoryID: categoryID,
businessID: businessID,
name: cat.name,
sortOrder: val(cat.sortOrder ?: 0)
});
} else {
// Insert new category as Item
queryExecute("
INSERT INTO Items (
ItemBusinessID, ItemName, ItemDescription,
ItemParentItemID, ItemPrice, ItemIsActive,
ItemSortOrder, ItemIsModifierTemplate, ItemAddedOn
) VALUES (
:businessID, :name, '',
0, 0, 1,
:sortOrder, 0, NOW()
)
", {
businessID: businessID,
name: cat.name,
sortOrder: val(cat.sortOrder ?: 0)
});
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
categoryID = result.newID;
}
} else {
// OLD SCHEMA: Use Categories table
if (categoryDbId > 0) {
categoryID = categoryDbId;
queryExecute("
UPDATE Categories
SET CategoryName = :name,
CategoryDescription = :description,
CategorySortOrder = :sortOrder
WHERE CategoryID = :categoryID
", {
categoryID: categoryID,
name: cat.name,
description: cat.description ?: "",
sortOrder: val(cat.sortOrder ?: 0)
});
} else {
queryExecute("
INSERT INTO Categories (CategoryBusinessID, CategoryName, CategoryDescription, CategorySortOrder)
VALUES (:businessID, :name, :description, :sortOrder)
", {
businessID: businessID,
name: cat.name,
description: cat.description ?: "",
sortOrder: val(cat.sortOrder ?: 0)
});
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
categoryID = result.newID;
}
}
// Process items in this category
if (structKeyExists(cat, "items") && isArray(cat.items)) {
for (item in cat.items) {
itemID = 0;
itemDbId = structKeyExists(item, "dbId") ? val(item.dbId) : 0;
if (itemDbId > 0) {
itemID = itemDbId;
if (newSchemaActive) {
// Update existing item - set parent to category Item
queryExecute("
UPDATE Items
SET ItemName = :name,
ItemDescription = :description,
ItemPrice = :price,
ItemParentItemID = :categoryID,
ItemSortOrder = :sortOrder
WHERE ItemID = :itemID
", {
itemID: itemID,
name: item.name,
description: item.description ?: "",
price: val(item.price ?: 0),
categoryID: categoryID,
sortOrder: val(item.sortOrder ?: 0)
});
} else {
// Update existing item - old schema with CategoryID
queryExecute("
UPDATE Items
SET ItemName = :name,
ItemDescription = :description,
ItemPrice = :price,
ItemCategoryID = :categoryID,
ItemSortOrder = :sortOrder
WHERE ItemID = :itemID
", {
itemID: itemID,
name: item.name,
description: item.description ?: "",
price: val(item.price ?: 0),
categoryID: categoryID,
sortOrder: val(item.sortOrder ?: 0)
});
}
} else {
// Insert new item
if (newSchemaActive) {
queryExecute("
INSERT INTO Items (
ItemBusinessID, ItemParentItemID, ItemName, ItemDescription,
ItemPrice, ItemSortOrder, ItemIsActive, ItemAddedOn
) VALUES (
:businessID, :categoryID, :name, :description,
:price, :sortOrder, 1, NOW()
)
", {
businessID: businessID,
categoryID: categoryID,
name: item.name,
description: item.description ?: "",
price: val(item.price ?: 0),
sortOrder: val(item.sortOrder ?: 0)
});
} else {
queryExecute("
INSERT INTO Items (
ItemBusinessID, ItemCategoryID, ItemName, ItemDescription,
ItemPrice, ItemSortOrder, ItemIsActive, ItemParentItemID
) VALUES (
:businessID, :categoryID, :name, :description,
:price, :sortOrder, 1, 0
)
", {
businessID: businessID,
categoryID: categoryID,
name: item.name,
description: item.description ?: "",
price: val(item.price ?: 0),
sortOrder: val(item.sortOrder ?: 0)
});
}
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
itemID = result.newID;
}
// Handle template links for modifiers
if (structKeyExists(item, "modifiers") && isArray(item.modifiers)) {
// Clear existing template links for this item
queryExecute("
DELETE FROM ItemTemplateLinks WHERE ItemID = :itemID
", { itemID: itemID });
modSortOrder = 0;
for (mod in item.modifiers) {
modDbId = structKeyExists(mod, "dbId") ? val(mod.dbId) : 0;
// Get selection rules (for modifier groups)
requiresSelection = (structKeyExists(mod, "requiresSelection") && mod.requiresSelection) ? 1 : 0;
maxSelections = structKeyExists(mod, "maxSelections") ? val(mod.maxSelections) : 0;
// Check if this is a template reference
if (structKeyExists(mod, "isTemplate") && mod.isTemplate && modDbId > 0) {
// Create template link
queryExecute("
INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder)
VALUES (:itemID, :templateID, :sortOrder)
ON DUPLICATE KEY UPDATE SortOrder = :sortOrder
", {
itemID: itemID,
templateID: modDbId,
sortOrder: modSortOrder
});
// Also update the template's selection rules
queryExecute("
UPDATE Items
SET ItemRequiresChildSelection = :requiresSelection,
ItemMaxNumSelectionReq = :maxSelections
WHERE ItemID = :modID
", {
modID: modDbId,
requiresSelection: requiresSelection,
maxSelections: maxSelections
});
} else if (modDbId > 0) {
// Update existing direct modifier
queryExecute("
UPDATE Items
SET ItemName = :name,
ItemPrice = :price,
ItemIsCheckedByDefault = :isDefault,
ItemSortOrder = :sortOrder,
ItemRequiresChildSelection = :requiresSelection,
ItemMaxNumSelectionReq = :maxSelections
WHERE ItemID = :modID
", {
modID: modDbId,
name: mod.name,
price: val(mod.price ?: 0),
isDefault: (mod.isDefault ?: false) ? 1 : 0,
sortOrder: modSortOrder,
requiresSelection: requiresSelection,
maxSelections: maxSelections
});
} else {
// Insert new direct modifier (non-template)
queryExecute("
INSERT INTO Items (
ItemBusinessID, ItemParentItemID, ItemName, ItemPrice,
ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive, ItemAddedOn,
ItemRequiresChildSelection, ItemMaxNumSelectionReq
) VALUES (
:businessID, :parentID, :name, :price,
:isDefault, :sortOrder, 1, NOW(),
:requiresSelection, :maxSelections
)
", {
businessID: businessID,
parentID: itemID,
name: mod.name,
price: val(mod.price ?: 0),
isDefault: (mod.isDefault ?: false) ? 1 : 0,
sortOrder: modSortOrder,
requiresSelection: requiresSelection,
maxSelections: maxSelections
});
}
modSortOrder++;
}
}
}
}
}
response = { "OK": true, "SCHEMA": newSchemaActive ? "unified" : "legacy" };
} catch (any e) {
response = {
"OK": false,
"ERROR": e.message,
"DETAIL": e.detail ?: "",
"TYPE": e.type ?: ""
};
}
cfheader(name="Content-Type", value="application/json");
writeOutput(serializeJSON(response));
</cfscript>