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/menu/items.cfm
John Mizerek 6b66d2cef8 Fix normalized DB column names across all API files
Sweep of 26 API files to use prefixed column names matching the
database schema (e.g. BusinessID not ID, BusinessName not Name,
BusinessDeliveryFlatFee not DeliveryFlatFee, ServicePointName not Name).

Files fixed: auth, beacons, businesses, menu, orders, setup, stripe,
tasks, and workers endpoints.

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

567 lines
20 KiB
Text

<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cffunction name="readJsonBody" access="public" returntype="struct" output="false">
<cfset var raw = getHttpRequestData().content>
<cfif isNull(raw) OR len(trim(raw)) EQ 0>
<cfreturn {}>
</cfif>
<cftry>
<cfset var data = deserializeJSON(raw)>
<cfif isStruct(data)>
<cfreturn data>
<cfelse>
<cfreturn {}>
</cfif>
<cfcatch>
<cfreturn {}>
</cfcatch>
</cftry>
</cffunction>
<cffunction name="apiAbort" access="public" returntype="void" output="true">
<cfargument name="payload" type="struct" required="true">
<cfcontent type="application/json; charset=utf-8">
<cfoutput>#serializeJSON(arguments.payload)#</cfoutput>
<cfabort>
</cffunction>
<cfset data = readJsonBody()>
<cfset BusinessID = 0>
<cfif structKeyExists(data, "BusinessID")>
<cfset BusinessID = val(data.BusinessID)>
</cfif>
<!--- Optional OrderTypeID for channel filtering (1=Dine-In, 2=Takeaway, 3=Delivery) --->
<cfset OrderTypeID = 0>
<cfif structKeyExists(data, "OrderTypeID")>
<cfset OrderTypeID = val(data.OrderTypeID)>
</cfif>
<!--- Optional MenuID for menu filtering (0 = all active menus) --->
<cfset requestedMenuID = 0>
<cfif structKeyExists(data, "MenuID")>
<cfset requestedMenuID = val(data.MenuID)>
</cfif>
<cfif BusinessID LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "missing_businessid", "MESSAGE": "BusinessID is required.", "DETAIL": "" })>
</cfif>
<!--- Get current time and day for schedule filtering --->
<cfset currentTime = timeFormat(now(), "HH:mm:ss")>
<cfset currentDayID = dayOfWeek(now())>
<cfset menuList = []>
<cftry>
<!--- Check if new schema is active (BusinessID column exists and has data) --->
<cfset newSchemaActive = false>
<cftry>
<cfset qCheck = queryExecute(
"SELECT COUNT(*) as cnt FROM Items WHERE BusinessID = ? AND BusinessID > 0",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset newSchemaActive = (qCheck.cnt GT 0)>
<cfcatch>
<!--- Column doesn't exist yet, use old schema --->
<cfset newSchemaActive = false>
</cfcatch>
</cftry>
<cfif newSchemaActive>
<!--- NEW SCHEMA: Items have BusinessID, try Categories table first, fallback to parent Items --->
<!--- Check if Categories table has data for this business --->
<cfset hasCategoriesData = false>
<cftry>
<cfset qCatCheck = queryExecute(
"SELECT COUNT(*) as cnt FROM Categories WHERE BusinessID = ?",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfset hasCategoriesData = (qCatCheck.cnt GT 0)>
<cfcatch>
<cfset hasCategoriesData = false>
</cfcatch>
</cftry>
<cfif hasCategoriesData>
<!--- Use Categories table with CategoryID --->
<!--- Get all active menus for this business (for chip selector) --->
<cfset activeMenuIds = "">
<cfset menuList = []>
<cftry>
<cfset qAllMenus = queryExecute(
"
SELECT ID, Name FROM Menus
WHERE BusinessID = :bizId
AND IsActive = 1
ORDER BY SortOrder, Name
",
{
bizId: { value = BusinessID, cfsqltype = "cf_sql_integer" }
},
{ datasource = "payfrit" }
)>
<cfif requestedMenuID GT 0>
<!--- User selected a specific menu --->
<cfset activeMenuIds = requestedMenuID>
<cfelse>
<!--- No specific menu selected — show all items from all menus --->
<cfset activeMenuIds = valueList(qAllMenus.ID)>
</cfif>
<!--- Build menu list for the response (all active menus, always) --->
<cfloop query="qAllMenus">
<cfset arrayAppend(menuList, {
"MenuID": qAllMenus.ID,
"Name": qAllMenus.Name
})>
</cfloop>
<cfcatch>
<!--- Menus table might not exist yet --->
</cfcatch>
</cftry>
<!--- Get category headers as virtual items --->
<!--- Apply schedule filtering, order type filtering, and menu filtering --->
<cfset qCategories = queryExecute(
"
SELECT
ID,
Name,
SortOrder,
OrderTypes,
ScheduleStart,
ScheduleEnd,
ScheduleDays,
MenuID
FROM Categories
WHERE BusinessID = :bizId
AND (
:orderTypeId = 0
OR FIND_IN_SET(:orderTypeId, OrderTypes) > 0
)
AND (
ScheduleStart IS NULL
OR ScheduleEnd IS NULL
OR (
TIME(:currentTime) >= ScheduleStart
AND TIME(:currentTime) <= ScheduleEnd
)
)
AND (
ScheduleDays IS NULL
OR ScheduleDays = ''
OR FIND_IN_SET(:currentDay, ScheduleDays) > 0
)
AND (
MenuID IS NULL
OR MenuID = 0
#len(activeMenuIds) ? "OR MenuID IN (#activeMenuIds#)" : ""#
)
ORDER BY SortOrder
",
{
bizId: { value = BusinessID, cfsqltype = "cf_sql_integer" },
orderTypeId: { value = OrderTypeID, cfsqltype = "cf_sql_integer" },
currentTime: { value = currentTime, cfsqltype = "cf_sql_varchar" },
currentDay: { value = currentDayID, cfsqltype = "cf_sql_integer" }
},
{ datasource = "payfrit" }
)>
<!--- Get menu items --->
<!--- Exclude old-style category headers (ParentID=0 AND CategoryID=0 AND no children with CategoryID>0) --->
<!--- These are legacy category headers that should be replaced by Categories table entries --->
<!--- Only include items from visible categories (after schedule/channel filtering) --->
<cfset visibleCategoryIds = valueList(qCategories.ID)>
<cfif len(trim(visibleCategoryIds)) EQ 0>
<cfset visibleCategoryIds = "0">
</cfif>
<cfset q = queryExecute(
"
SELECT
i.ID,
i.CategoryID,
c.Name AS CategoryName,
i.Name AS ItemName,
i.Description,
i.ParentItemID,
i.Price,
i.IsActive,
i.IsCheckedByDefault,
i.RequiresChildSelection,
i.MaxNumSelectionReq,
i.IsCollapsible,
i.SortOrder,
i.StationID,
s.Name AS StationName,
s.Color,
c.MenuID
FROM Items i
LEFT JOIN Categories c ON c.ID = i.CategoryID
LEFT JOIN Stations s ON s.ID = i.StationID
WHERE i.BusinessID = :bizId
AND i.IsActive = 1
AND i.CategoryID IN (#visibleCategoryIds#)
AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = i.ID)
AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl2 WHERE tl2.TemplateItemID = i.ParentItemID)
AND NOT (i.ParentItemID = 0 AND i.CategoryID = 0 AND i.Price = 0)
ORDER BY COALESCE(c.SortOrder, 999), i.SortOrder, i.ID
",
{ bizId: { value = BusinessID, cfsqltype = "cf_sql_integer" } },
{ datasource = "payfrit" }
)>
<cfelse>
<!--- Fallback: Derive categories from parent Items --->
<cfset q = queryExecute(
"
SELECT
i.ID,
CASE
WHEN i.ParentItemID = 0 AND i.IsCollapsible = 0 THEN i.ID
ELSE COALESCE(
(SELECT cat.ID FROM Items cat
WHERE cat.ID = i.ParentItemID
AND cat.ParentItemID = 0
AND cat.IsCollapsible = 0),
0
)
END as CategoryID,
CASE
WHEN i.ParentItemID = 0 AND i.IsCollapsible = 0 THEN i.Name
ELSE COALESCE(
(SELECT cat.Name FROM Items cat
WHERE cat.ID = i.ParentItemID
AND cat.ParentItemID = 0
AND cat.IsCollapsible = 0),
''
)
END as Name,
i.Name,
i.Description,
i.ParentItemID,
i.Price,
i.IsActive,
i.IsCheckedByDefault,
i.RequiresChildSelection,
i.MaxNumSelectionReq,
i.IsCollapsible,
i.SortOrder,
i.StationID,
s.Name,
s.Color
FROM Items i
LEFT JOIN Stations s ON s.ID = i.StationID
WHERE i.BusinessID = ?
AND i.IsActive = 1
AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = i.ID)
AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl2 WHERE tl2.TemplateItemID = i.ParentItemID)
AND (
i.ParentItemID > 0
OR (i.ParentItemID = 0 AND i.IsCollapsible = 0)
)
ORDER BY i.ParentItemID, i.SortOrder, i.ID
",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
</cfif>
<cfelse>
<!--- OLD SCHEMA: Use Categories table --->
<cfset q = queryExecute(
"
SELECT
i.ID,
i.CategoryID,
c.Name,
i.Name,
i.Description,
i.ParentItemID,
i.Price,
i.IsActive,
i.IsCheckedByDefault,
i.RequiresChildSelection,
i.MaxNumSelectionReq,
i.IsCollapsible,
i.SortOrder,
i.StationID,
s.Name,
s.Color
FROM Items i
INNER JOIN Categories c ON c.ID = i.CategoryID
LEFT JOIN Stations s ON s.ID = i.StationID
WHERE c.BusinessID = ?
ORDER BY i.ParentItemID, i.SortOrder, i.ID
",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
</cfif>
<cfset rows = []>
<!--- Build set of category IDs that actually have items --->
<cfset categoriesWithItems = {}>
<cfloop query="q">
<cfif q.CategoryID GT 0>
<cfset categoriesWithItems[q.CategoryID] = true>
</cfif>
</cfloop>
<!--- For unified schema with Categories table, add category headers (only if they have items) --->
<cfif newSchemaActive AND isDefined("qCategories")>
<cfloop query="qCategories">
<cfif structKeyExists(categoriesWithItems, qCategories.ID)>
<!--- Add category as a virtual parent item --->
<cfset arrayAppend(rows, {
"ItemID": qCategories.ID,
"CategoryID": qCategories.ID,
"Name": qCategories.Name,
"Description": "",
"ParentItemID": 0,
"Price": 0,
"IsActive": 1,
"IsCheckedByDefault": 0,
"RequiresChildSelection": 0,
"MaxNumSelectionReq": 0,
"IsCollapsible": 0,
"SortOrder": qCategories.SortOrder,
"MenuID": isNull(qCategories.MenuID) ? 0 : val(qCategories.MenuID),
"StationID": "",
"ItemName": "",
"ItemColor": ""
})>
</cfif>
</cfloop>
</cfif>
<!--- Build a set of category IDs for quick lookup --->
<cfset categoryIdSet = {}>
<cfif isDefined("qCategories")>
<cfloop query="qCategories">
<cfset categoryIdSet[qCategories.ID] = true>
</cfloop>
</cfif>
<cfloop query="q">
<!--- For unified schema with Categories: set ParentItemID to CategoryID for top-level items --->
<!--- Remap old parent IDs to category IDs --->
<cfset effectiveParentID = q.ParentItemID>
<cfif newSchemaActive AND isDefined("qCategories") AND q.CategoryID GT 0>
<cfif q.ParentItemID EQ 0>
<!--- Item has no parent but has a category - link to category --->
<cfset effectiveParentID = q.CategoryID>
<cfelseif structKeyExists(categoryIdSet, q.ParentItemID)>
<!--- Item's parent IS a category ID - this is correct, keep it --->
<cfset effectiveParentID = q.ParentItemID>
<cfelseif NOT structKeyExists(categoryIdSet, q.ParentItemID)>
<!--- Parent ID is an old-style category header - remap to CategoryID --->
<cfset effectiveParentID = q.CategoryID>
</cfif>
</cfif>
<cfset itemMenuID = 0>
<cftry>
<cfset itemMenuID = isNull(q.MenuID) ? 0 : val(q.MenuID)>
<cfcatch><cfset itemMenuID = 0></cfcatch>
</cftry>
<!--- Use aliased columns if available, fall back to generic Name --->
<cfset itemName = "">
<cfset catName = "">
<cftry>
<cfset itemName = q.ItemName>
<cfset catName = q.CategoryName>
<cfcatch>
<!--- Fallback for old schema queries without aliases --->
<cfset itemName = q.Name>
<cfset catName = q.Name>
</cfcatch>
</cftry>
<cfset arrayAppend(rows, {
"ItemID": q.ID,
"CategoryID": q.CategoryID,
"Name": len(trim(itemName)) ? itemName : catName,
"Description": q.Description,
"ParentItemID": effectiveParentID,
"Price": q.Price,
"IsActive": q.IsActive,
"IsCheckedByDefault": q.IsCheckedByDefault,
"RequiresChildSelection": q.RequiresChildSelection,
"MaxNumSelectionReq": q.MaxNumSelectionReq,
"IsCollapsible": q.IsCollapsible,
"SortOrder": q.SortOrder,
"MenuID": itemMenuID,
"StationID": len(trim(q.StationID)) ? q.StationID : "",
"ItemName": len(trim(catName)) ? catName : "",
"ItemColor": len(trim(q.Color)) ? q.Color : ""
})>
</cfloop>
<!--- For unified schema: Add template-linked modifiers as virtual children of menu items --->
<cfif newSchemaActive>
<!--- Get template links: which menu items use which templates --->
<cfset qTemplateLinks = queryExecute(
"
SELECT
tl.ItemID as MenuItemID,
tmpl.ID as TemplateItemID,
tmpl.Name as TemplateName,
tmpl.Description as TemplateDescription,
tmpl.RequiresChildSelection as TemplateRequired,
tmpl.MaxNumSelectionReq as TemplateMaxSelections,
tmpl.IsCollapsible as TemplateIsCollapsible,
tl.SortOrder as TemplateSortOrder
FROM lt_ItemID_TemplateItemID tl
INNER JOIN Items tmpl ON tmpl.ID = tl.TemplateItemID AND tmpl.IsActive = 1
INNER JOIN Items menuItem ON menuItem.ID = tl.ItemID
WHERE menuItem.BusinessID = ?
AND menuItem.IsActive = 1
ORDER BY tl.ItemID, tl.SortOrder
",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<!--- Get template options --->
<cfset qTemplateOptions = queryExecute(
"
SELECT DISTINCT
opt.ID as OptionItemID,
opt.ParentItemID as TemplateItemID,
opt.Name as OptionName,
opt.Description as OptionDescription,
opt.Price as OptionPrice,
opt.IsCheckedByDefault as OptionIsDefault,
opt.SortOrder as OptionSortOrder
FROM Items opt
INNER JOIN lt_ItemID_TemplateItemID tl ON tl.TemplateItemID = opt.ParentItemID
INNER JOIN Items menuItem ON menuItem.ID = tl.ItemID
WHERE menuItem.BusinessID = ?
AND menuItem.IsActive = 1
AND opt.IsActive = 1
ORDER BY opt.ParentItemID, opt.SortOrder
",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<!--- Build template options map: templateID -> [options] --->
<cfset templateOptionsMap = {}>
<cfloop query="qTemplateOptions">
<cfif NOT structKeyExists(templateOptionsMap, qTemplateOptions.TemplateItemID)>
<cfset templateOptionsMap[qTemplateOptions.TemplateItemID] = []>
</cfif>
<cfset arrayAppend(templateOptionsMap[qTemplateOptions.TemplateItemID], {
"ItemID": qTemplateOptions.OptionItemID,
"Name": qTemplateOptions.OptionName,
"Description": qTemplateOptions.OptionDescription,
"Price": qTemplateOptions.OptionPrice,
"IsCheckedByDefault": qTemplateOptions.OptionIsDefault,
"SortOrder": qTemplateOptions.OptionSortOrder
})>
</cfloop>
<!--- Add templates and their options as virtual children --->
<!--- Use virtual IDs to make each template instance unique per menu item --->
<!--- Virtual ID format: menuItemID * 100000 + templateID for templates --->
<!--- menuItemID * 100000 + optionID for options --->
<cfset addedTemplates = {}>
<cfloop query="qTemplateLinks">
<cfset menuItemID = qTemplateLinks.MenuItemID>
<cfset templateID = qTemplateLinks.TemplateItemID>
<cfset linkKey = menuItemID & "_" & templateID>
<!--- Skip duplicates --->
<cfif structKeyExists(addedTemplates, linkKey)>
<cfcontinue>
</cfif>
<cfset addedTemplates[linkKey] = true>
<!--- Generate unique virtual ID for this template instance --->
<cfset virtualTemplateID = menuItemID * 100000 + templateID>
<!--- Add template as modifier group (child of menu item) --->
<cfset arrayAppend(rows, {
"ItemID": virtualTemplateID,
"CategoryID": 0,
"Name": "",
"Name": qTemplateLinks.TemplateName,
"Description": qTemplateLinks.TemplateDescription,
"ParentItemID": menuItemID,
"Price": 0,
"IsActive": 1,
"IsCheckedByDefault": 0,
"RequiresChildSelection": qTemplateLinks.TemplateRequired,
"MaxNumSelectionReq": qTemplateLinks.TemplateMaxSelections,
"IsCollapsible": qTemplateLinks.TemplateIsCollapsible,
"SortOrder": qTemplateLinks.TemplateSortOrder,
"StationID": "",
"ItemName": "",
"ItemColor": ""
})>
<!--- Add template options as children of virtual template --->
<cfif structKeyExists(templateOptionsMap, templateID)>
<cfloop array="#templateOptionsMap[templateID]#" index="opt">
<!--- Generate unique virtual ID for this option instance --->
<cfset virtualOptionID = menuItemID * 100000 + opt.ItemID>
<cfset arrayAppend(rows, {
"ItemID": virtualOptionID,
"CategoryID": 0,
"Name": "",
"Name": opt.Name,
"Description": opt.Description,
"ParentItemID": virtualTemplateID,
"Price": opt.Price,
"IsActive": 1,
"IsCheckedByDefault": opt.IsCheckedByDefault,
"RequiresChildSelection": 0,
"MaxNumSelectionReq": 0,
"IsCollapsible": 0,
"SortOrder": opt.SortOrder,
"StationID": "",
"ItemName": "",
"ItemColor": ""
})>
</cfloop>
</cfif>
</cfloop>
</cfif>
<!--- Get brand color for this business --->
<cfset brandColor = "">
<cftry>
<cfset qBrand = queryExecute(
"SELECT BusinessBrandColor FROM Businesses WHERE BusinessID = ?",
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
{ datasource = "payfrit" }
)>
<cfif qBrand.recordCount GT 0 AND len(trim(qBrand.BusinessBrandColor))>
<cfset brandColor = left(qBrand.BusinessBrandColor, 1) EQ chr(35) ? qBrand.BusinessBrandColor : chr(35) & qBrand.BusinessBrandColor>
</cfif>
<cfcatch>
<!--- Column may not exist yet, ignore --->
</cfcatch>
</cftry>
<cfset apiAbort({
"OK": true,
"ERROR": "",
"Items": rows,
"COUNT": arrayLen(rows),
"SCHEMA": newSchemaActive ? "unified" : "legacy",
"BRANDCOLOR": brandColor,
"Menus": menuList,
"SelectedMenuID": requestedMenuID
})>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "DB error loading items",
"DETAIL": cfcatch.message
})>
</cfcatch>
</cftry>