Compare commits

...

2 commits

Author SHA1 Message Date
John Mizerek
28c0de9f09 Remove IsInvertedGroup feature from all APIs, KDS, and portal
Feature cancelled — modifier wording handles the use case instead.
Removes IsInvertedGroup from SELECTs, JSON responses, RemovedDefaults
computation, and KDS/portal display logic. DB column left in place.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:53:09 -07:00
John Mizerek
bace74aa23 Remove Uber Eats JSON-LD fast path — let Claude extract modifiers
The JSON-LD fast path only got items/categories/prices but no modifiers.
Removing it lets Uber Eats pages fall through to Claude AI extraction
which handles modifiers like every other platform.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 00:08:57 -07:00
11 changed files with 24 additions and 301 deletions

View file

@ -26,7 +26,6 @@ function buildOptionsTree(allOptions, parentId) {
"sortOrder": allOptions.SortOrder[i],
"requiresSelection": isNull(allOptions.RequiresSelection[i]) ? false : (allOptions.RequiresSelection[i] == 1),
"maxSelections": isNull(allOptions.MaxSelections[i]) ? 0 : allOptions.MaxSelections[i],
"isInverted": structKeyExists(allOptions, "IsInvertedGroup") && !isNull(allOptions.IsInvertedGroup[i]) ? (allOptions.IsInvertedGroup[i] == 1) : false,
"options": children
});
}
@ -196,8 +195,7 @@ try {
m.IsCheckedByDefault as IsDefault,
m.SortOrder,
m.RequiresChildSelection as RequiresSelection,
m.MaxNumSelectionReq as MaxSelections,
m.IsInvertedGroup
m.MaxNumSelectionReq as MaxSelections
FROM Items m
WHERE m.BusinessID = :businessID
AND m.IsActive = 1
@ -253,8 +251,7 @@ try {
m.IsCheckedByDefault as IsDefault,
m.SortOrder,
m.RequiresChildSelection as RequiresSelection,
m.MaxNumSelectionReq as MaxSelections,
m.IsInvertedGroup
m.MaxNumSelectionReq as MaxSelections
FROM Items m
WHERE m.BusinessID = :businessID
AND m.IsActive = 1
@ -292,8 +289,7 @@ try {
t.IsCheckedByDefault as IsDefault,
t.SortOrder,
t.RequiresChildSelection as RequiresSelection,
t.MaxNumSelectionReq as MaxSelections,
t.IsInvertedGroup
t.MaxNumSelectionReq as MaxSelections
FROM Items t
WHERE t.BusinessID = :businessID
AND (t.CategoryID = 0 OR t.CategoryID IS NULL)
@ -308,7 +304,7 @@ try {
arrayAppend(templateIds, qTemplates.ItemID[i]);
}
qTemplateChildren = queryNew("ItemID,ParentItemID,Name,Price,IsDefault,SortOrder,RequiresSelection,MaxSelections,IsInvertedGroup");
qTemplateChildren = queryNew("ItemID,ParentItemID,Name,Price,IsDefault,SortOrder,RequiresSelection,MaxSelections");
if (arrayLen(templateIds) > 0) {
qTemplateChildren = queryTimed("
SELECT
@ -319,8 +315,7 @@ try {
c.IsCheckedByDefault as IsDefault,
c.SortOrder,
c.RequiresChildSelection as RequiresSelection,
c.MaxNumSelectionReq as MaxSelections,
c.IsInvertedGroup
c.MaxNumSelectionReq as MaxSelections
FROM Items c
WHERE c.ParentItemID IN (:templateIds)
AND c.IsActive = 1
@ -343,7 +338,6 @@ try {
"isTemplate": true,
"requiresSelection": isNull(qTemplates.RequiresSelection[i]) ? false : (qTemplates.RequiresSelection[i] == 1),
"maxSelections": isNull(qTemplates.MaxSelections[i]) ? 0 : qTemplates.MaxSelections[i],
"isInverted": isNull(qTemplates.IsInvertedGroup[i]) ? false : (qTemplates.IsInvertedGroup[i] == 1),
"options": options
};
}

View file

@ -208,7 +208,6 @@
i.RequiresChildSelection,
i.MaxNumSelectionReq,
i.IsCollapsible,
i.IsInvertedGroup,
i.SortOrder,
i.StationID,
s.Name AS StationName,
@ -263,7 +262,6 @@
i.RequiresChildSelection,
i.MaxNumSelectionReq,
i.IsCollapsible,
i.IsInvertedGroup,
i.SortOrder,
i.StationID,
s.Name,
@ -301,7 +299,6 @@
i.RequiresChildSelection,
i.MaxNumSelectionReq,
i.IsCollapsible,
i.IsInvertedGroup,
i.SortOrder,
i.StationID,
s.Name,
@ -354,7 +351,6 @@
"RequiresChildSelection": 0,
"MaxNumSelectionReq": 0,
"IsCollapsible": 0,
"IsInvertedGroup": 0,
"SortOrder": qCategories.SortOrder,
"MenuID": isNull(qCategories.MenuID) ? 0 : val(qCategories.MenuID),
"StationID": "",
@ -419,7 +415,6 @@
"RequiresChildSelection": q.RequiresChildSelection,
"MaxNumSelectionReq": q.MaxNumSelectionReq,
"IsCollapsible": q.IsCollapsible,
"IsInvertedGroup": q.IsInvertedGroup,
"SortOrder": q.SortOrder,
"MenuID": itemMenuID,
"StationID": len(trim(q.StationID)) ? q.StationID : "",
@ -441,7 +436,6 @@
tmpl.RequiresChildSelection as TemplateRequired,
tmpl.MaxNumSelectionReq as TemplateMaxSelections,
tmpl.IsCollapsible as TemplateIsCollapsible,
tmpl.IsInvertedGroup as TemplateIsInvertedGroup,
tl.SortOrder as TemplateSortOrder
FROM lt_ItemID_TemplateItemID tl
INNER JOIN Items tmpl ON tmpl.ID = tl.TemplateItemID AND tmpl.IsActive = 1
@ -526,7 +520,6 @@
"RequiresChildSelection": qTemplateLinks.TemplateRequired,
"MaxNumSelectionReq": qTemplateLinks.TemplateMaxSelections,
"IsCollapsible": qTemplateLinks.TemplateIsCollapsible,
"IsInvertedGroup": qTemplateLinks.TemplateIsInvertedGroup,
"SortOrder": qTemplateLinks.TemplateSortOrder,
"StationID": "",
"ItemName": "",
@ -551,7 +544,6 @@
"RequiresChildSelection": 0,
"MaxNumSelectionReq": 0,
"IsCollapsible": 0,
"IsInvertedGroup": 0,
"SortOrder": opt.SortOrder,
"StationID": "",
"ItemName": "",

View file

@ -19,7 +19,6 @@ function saveOptionsRecursive(options, parentID, businessID) {
var requiresSelection = (structKeyExists(opt, "requiresSelection") && opt.requiresSelection) ? 1 : 0;
var maxSelections = structKeyExists(opt, "maxSelections") ? val(opt.maxSelections) : 0;
var isDefault = (structKeyExists(opt, "isDefault") && opt.isDefault) ? 1 : 0;
var isInverted = (structKeyExists(opt, "isInverted") && opt.isInverted) ? 1 : 0;
var optionID = 0;
if (optDbId > 0) {
@ -32,7 +31,6 @@ function saveOptionsRecursive(options, parentID, businessID) {
SortOrder = :sortOrder,
RequiresChildSelection = :requiresSelection,
MaxNumSelectionReq = :maxSelections,
IsInvertedGroup = :isInverted,
ParentItemID = :parentID
WHERE ID = :optID
", {
@ -43,19 +41,18 @@ function saveOptionsRecursive(options, parentID, businessID) {
isDefault: isDefault,
sortOrder: optSortOrder,
requiresSelection: requiresSelection,
maxSelections: maxSelections,
isInverted: isInverted
maxSelections: maxSelections
});
} else {
queryTimed("
INSERT INTO Items (
BusinessID, ParentItemID, Name, Price,
IsCheckedByDefault, SortOrder, IsActive, AddedOn,
RequiresChildSelection, MaxNumSelectionReq, IsInvertedGroup, CategoryID
RequiresChildSelection, MaxNumSelectionReq, CategoryID
) VALUES (
:businessID, :parentID, :name, :price,
:isDefault, :sortOrder, 1, NOW(),
:requiresSelection, :maxSelections, :isInverted, 0
:requiresSelection, :maxSelections, 0
)
", {
businessID: businessID,
@ -65,8 +62,7 @@ function saveOptionsRecursive(options, parentID, businessID) {
isDefault: isDefault,
sortOrder: optSortOrder,
requiresSelection: requiresSelection,
maxSelections: maxSelections,
isInverted: isInverted
maxSelections: maxSelections
});
var result = queryTimed("SELECT LAST_INSERT_ID() as newID");
@ -338,15 +334,13 @@ try {
UPDATE Items
SET Name = :name,
RequiresChildSelection = :requiresSelection,
MaxNumSelectionReq = :maxSelections,
IsInvertedGroup = :isInverted
MaxNumSelectionReq = :maxSelections
WHERE ID = :modID
", {
modID: modDbId,
name: mod.name,
requiresSelection: requiresSelection,
maxSelections: maxSelections,
isInverted: (structKeyExists(mod, "isInverted") && mod.isInverted) ? 1 : 0
maxSelections: maxSelections
});
// Only save template options ONCE (first time we encounter this template)
@ -366,7 +360,6 @@ try {
SortOrder = :sortOrder,
RequiresChildSelection = :requiresSelection,
MaxNumSelectionReq = :maxSelections,
IsInvertedGroup = :isInverted,
ParentItemID = :parentID
WHERE ID = :modID
", {
@ -377,8 +370,7 @@ try {
isDefault: (mod.isDefault ?: false) ? 1 : 0,
sortOrder: modSortOrder,
requiresSelection: requiresSelection,
maxSelections: maxSelections,
isInverted: (structKeyExists(mod, "isInverted") && mod.isInverted) ? 1 : 0
maxSelections: maxSelections
});
if (structKeyExists(mod, "options") && isArray(mod.options)) {
@ -390,11 +382,11 @@ try {
INSERT INTO Items (
BusinessID, ParentItemID, Name, Price,
IsCheckedByDefault, SortOrder, IsActive, AddedOn,
RequiresChildSelection, MaxNumSelectionReq, IsInvertedGroup, CategoryID
RequiresChildSelection, MaxNumSelectionReq, CategoryID
) VALUES (
:businessID, :parentID, :name, :price,
:isDefault, :sortOrder, 1, NOW(),
:requiresSelection, :maxSelections, :isInverted, 0
:requiresSelection, :maxSelections, 0
)
", {
businessID: businessID,
@ -404,8 +396,7 @@ try {
isDefault: (mod.isDefault ?: false) ? 1 : 0,
sortOrder: modSortOrder,
requiresSelection: requiresSelection,
maxSelections: maxSelections,
isInverted: (structKeyExists(mod, "isInverted") && mod.isInverted) ? 1 : 0
maxSelections: maxSelections
});
modResult = queryTimed("SELECT LAST_INSERT_ID() as newModID");

View file

@ -95,9 +95,7 @@
i.Name,
i.ParentItemID,
i.IsCheckedByDefault,
i.IsInvertedGroup,
parent.Name AS ItemParentName,
parent.IsInvertedGroup AS ParentIsInvertedGroup
parent.Name AS ItemParentName
FROM OrderLineItems oli
INNER JOIN Items i ON i.ID = oli.ItemID
LEFT JOIN Items parent ON parent.ID = i.ParentItemID
@ -126,10 +124,7 @@
"Name": qLI.Name ?: "",
"ParentItemID": val(qLI.ParentItemID),
"ItemParentName": qLI.ItemParentName ?: "",
"IsCheckedByDefault": val(qLI.IsCheckedByDefault),
"IsInvertedGroup": val(qLI.IsInvertedGroup),
"ParentIsInvertedGroup": val(qLI.ParentIsInvertedGroup),
"_debug": "Qty=#val(qLI.Quantity)# IsChkDef=#val(qLI.IsCheckedByDefault)# ParentInv=#val(qLI.ParentIsInvertedGroup)#"
"IsCheckedByDefault": val(qLI.IsCheckedByDefault)
})>
<!--- Add to subtotal (root items only - modifiers are included in parent price) --->
<cfif val(qLI.ParentOrderLineItemID) EQ 0>

View file

@ -124,10 +124,8 @@
i.Name,
i.ParentItemID,
i.IsCheckedByDefault,
i.IsInvertedGroup,
i.StationID,
parent.Name AS ItemParentName,
COALESCE(parent.IsInvertedGroup, 0) AS ParentIsInvertedGroup
parent.Name AS ItemParentName
FROM OrderLineItems oli
INNER JOIN Items i ON i.ID = oli.ItemID
LEFT JOIN Items parent ON parent.ID = i.ParentItemID
@ -149,47 +147,11 @@
"ParentItemID": qLineItems.ParentItemID,
"ItemParentName": qLineItems.ItemParentName,
"IsCheckedByDefault": qLineItems.IsCheckedByDefault,
"IsInvertedGroup": qLineItems.IsInvertedGroup,
"ParentIsInvertedGroup": qLineItems.ParentIsInvertedGroup,
"StationID": qLineItems.StationID,
"StatusID": val(qLineItems.StatusID)
})>
</cfloop>
<!--- For inverted modifier groups, compute removed defaults --->
<!--- The inverted group header may not be an order line item itself,
so we detect inverted groups by finding children whose parent item is inverted --->
<cfset invertedGroupsSeen = {}>
<cfloop array="#lineItems#" index="li">
<cfif li.ParentIsInvertedGroup AND NOT structKeyExists(invertedGroupsSeen, li.ParentItemID)>
<!--- First child of this inverted group — compute removed defaults once --->
<cfset invertedGroupsSeen[li.ParentItemID] = true>
<cfset qRemovedDefaults = queryTimed("
SELECT i.Name
FROM Items i
WHERE i.ParentItemID = ?
AND i.IsActive = 1
AND i.IsCheckedByDefault = b'1'
AND i.ID NOT IN (
SELECT oli2.ItemID FROM OrderLineItems oli2
WHERE oli2.OrderID = ? AND oli2.ParentOrderLineItemID = ? AND oli2.IsDeleted = b'0'
)
ORDER BY i.SortOrder
", [
{ value = li.ParentItemID, cfsqltype = "cf_sql_integer" },
{ value = qOrders.ID, cfsqltype = "cf_sql_integer" },
{ value = li.ParentOrderLineItemID, cfsqltype = "cf_sql_integer" }
], { datasource = "payfrit" })>
<cfset removedNames = []>
<cfloop query="qRemovedDefaults">
<cfset arrayAppend(removedNames, qRemovedDefaults.Name)>
</cfloop>
<!--- Attach RemovedDefaults to the first child so the JS can find it --->
<cfset li["RemovedDefaults"] = removedNames>
<cfset li["IsInvertedGroupProxy"] = true>
</cfif>
</cfloop>
<!--- Determine order type name --->
<cfset orderTypeName = "">
<cfswitch expression="#qOrders.OrderTypeID#">

View file

@ -254,9 +254,7 @@
i.Name,
i.ParentItemID,
i.IsCheckedByDefault,
i.IsInvertedGroup,
parent.Name AS ItemParentName,
parent.IsInvertedGroup AS ParentIsInvertedGroup
parent.Name AS ItemParentName
FROM OrderLineItems oli
INNER JOIN Items i ON i.ID = oli.ItemID
LEFT JOIN Items parent ON parent.ID = i.ParentItemID
@ -283,9 +281,7 @@
"Name": qLI.Name ?: "",
"ParentItemID": val(qLI.ParentItemID),
"ItemParentName": qLI.ItemParentName ?: "",
"IsCheckedByDefault": val(qLI.IsCheckedByDefault),
"IsInvertedGroup": val(qLI.IsInvertedGroup),
"ParentIsInvertedGroup": val(qLI.ParentIsInvertedGroup)
"IsCheckedByDefault": val(qLI.IsCheckedByDefault)
})>
</cfloop>

View file

@ -2190,166 +2190,7 @@
</cfif>
<!--- ============================================================ --->
<!--- UBER EATS FAST PATH: Parse JSON-LD structured menu data --->
<!--- ============================================================ --->
<cfif findNoCase("ubereats.com", pageHtml) OR findNoCase("uber.com/store", pageHtml)>
<cfset arrayAppend(response.steps, "Uber Eats page detected - looking for JSON-LD menu data")>
<cftry>
<!--- Extract all JSON-LD blocks --->
<cfset jsonLdBlocks = reMatchNoCase('<script[^>]*type\s*=\s*["'']application/ld\+json["''][^>]*>([\s\S]*?)</script>', pageHtml)>
<cfset arrayAppend(response.steps, "Found " & arrayLen(jsonLdBlocks) & " JSON-LD blocks")>
<cfset ueRestaurant = "">
<cfloop array="#jsonLdBlocks#" index="ldBlock">
<!--- Extract content between script tags --->
<cfset ldContent = reReplaceNoCase(ldBlock, '<script[^>]*>([\s\S]*?)</script>', '\1')>
<cfset ldContent = trim(ldContent)>
<cfif len(ldContent)>
<cftry>
<!--- Unescape unicode \u002F etc --->
<cfset ldContent = replace(ldContent, '\u002F', '/', 'all')>
<cfset ldContent = replace(ldContent, '\u0026', '&', 'all')>
<cfset ldContent = replace(ldContent, '\u0022', '"', 'all')>
<cfset ldContent = replace(ldContent, '\u0027', "'", 'all')>
<cfset ldParsed = deserializeJSON(ldContent)>
<cfif isStruct(ldParsed) AND structKeyExists(ldParsed, "@type") AND ldParsed["@type"] EQ "Restaurant" AND structKeyExists(ldParsed, "hasMenu")>
<cfset ueRestaurant = ldParsed>
</cfif>
<cfcatch><!--- skip unparseable blocks ---></cfcatch>
</cftry>
</cfif>
</cfloop>
<cfif isStruct(ueRestaurant) AND structKeyExists(ueRestaurant, "hasMenu")>
<cfset arrayAppend(response.steps, "Found Restaurant JSON-LD with menu data")>
<!--- Parse business info --->
<cfset ueBusiness = structNew()>
<cfset ueBusiness["name"] = structKeyExists(ueRestaurant, "name") ? ueRestaurant.name : "">
<!--- Unescape HTML entities in name --->
<cfset ueBusiness["name"] = replace(ueBusiness.name, "&amp;", "&", "all")>
<cfset ueBusiness["name"] = replace(ueBusiness.name, "&lt;", "<", "all")>
<cfset ueBusiness["name"] = replace(ueBusiness.name, "&gt;", ">", "all")>
<cfset ueBusiness["name"] = replace(ueBusiness.name, "&##39;", "'", "all")>
<cfset ueBusiness["name"] = replace(ueBusiness.name, "&apos;", "'", "all")>
<cfif structKeyExists(ueRestaurant, "address") AND isStruct(ueRestaurant.address)>
<cfset ueAddr = ueRestaurant.address>
<cfif structKeyExists(ueAddr, "streetAddress")><cfset ueBusiness["addressLine1"] = ueAddr.streetAddress></cfif>
<cfif structKeyExists(ueAddr, "addressLocality")><cfset ueBusiness["city"] = ueAddr.addressLocality></cfif>
<cfif structKeyExists(ueAddr, "addressRegion")><cfset ueBusiness["state"] = ueAddr.addressRegion></cfif>
<cfif structKeyExists(ueAddr, "postalCode")><cfset ueBusiness["zip"] = ueAddr.postalCode></cfif>
</cfif>
<!--- Parse menu sections --->
<cfset ueMenu = ueRestaurant.hasMenu>
<cfset ueCategories = []>
<cfset ueItems = []>
<cfset ueImageMappings = structNew()>
<cfset ueItemId = 1>
<cfif structKeyExists(ueMenu, "hasMenuSection") AND isArray(ueMenu.hasMenuSection)>
<cfloop array="#ueMenu.hasMenuSection#" index="ueSection">
<cfset ueCatName = structKeyExists(ueSection, "name") ? trim(ueSection.name) : "Menu">
<!--- Unescape HTML entities --->
<cfset ueCatName = replace(ueCatName, "&amp;", "&", "all")>
<cfset ueCatName = replace(ueCatName, "&##39;", "'", "all")>
<cfset ueCatName = replace(ueCatName, "&apos;", "'", "all")>
<cfset ueSectionItemCount = 0>
<cfif structKeyExists(ueSection, "hasMenuItem") AND isArray(ueSection.hasMenuItem)>
<cfloop array="#ueSection.hasMenuItem#" index="ueMenuItem">
<cfset ueItemName = structKeyExists(ueMenuItem, "name") ? trim(ueMenuItem.name) : "">
<cfif NOT len(ueItemName)><cfcontinue></cfif>
<!--- Unescape HTML entities in name and description --->
<cfset ueItemName = replace(ueItemName, "&amp;", "&", "all")>
<cfset ueItemName = replace(ueItemName, "&##39;", "'", "all")>
<cfset ueItemName = replace(ueItemName, "&apos;", "'", "all")>
<cfset ueItemName = replace(ueItemName, "&lt;", "<", "all")>
<cfset ueItemName = replace(ueItemName, "&gt;", ">", "all")>
<cfset ueItemDesc = structKeyExists(ueMenuItem, "description") ? trim(ueMenuItem.description) : "">
<cfset ueItemDesc = replace(ueItemDesc, "&amp;", "&", "all")>
<cfset ueItemDesc = replace(ueItemDesc, "&##39;", "'", "all")>
<cfset ueItemDesc = replace(ueItemDesc, "&apos;", "'", "all")>
<!--- Extract price from offers --->
<cfset uePrice = 0>
<cfif structKeyExists(ueMenuItem, "offers") AND isStruct(ueMenuItem.offers)>
<cfif structKeyExists(ueMenuItem.offers, "price")>
<cfset uePrice = val(ueMenuItem.offers.price)>
</cfif>
</cfif>
<!--- Extract image if available --->
<cfset ueItemImage = "">
<cfif structKeyExists(ueMenuItem, "image") AND len(trim(ueMenuItem.image))>
<cfset ueItemImage = trim(ueMenuItem.image)>
<cfset ueImageMappings[ueItemName] = ueItemImage>
</cfif>
<cfset arrayAppend(ueItems, {
"id": "item_" & ueItemId,
"name": ueItemName,
"price": uePrice,
"description": ueItemDesc,
"category": ueCatName,
"modifiers": [],
"imageUrl": ueItemImage
})>
<cfset ueItemId = ueItemId + 1>
<cfset ueSectionItemCount = ueSectionItemCount + 1>
</cfloop>
</cfif>
<cfif ueSectionItemCount GT 0>
<cfset arrayAppend(ueCategories, { "name": ueCatName, "itemCount": ueSectionItemCount })>
</cfif>
</cfloop>
</cfif>
<cfset arrayAppend(response.steps, "Parsed " & arrayLen(ueItems) & " items in " & arrayLen(ueCategories) & " categories from Uber Eats JSON-LD")>
<cfif arrayLen(ueItems) GT 0>
<!--- Try to get restaurant header image from JSON-LD images --->
<cfset ueHeaderImage = "">
<cfif structKeyExists(ueRestaurant, "image") AND isArray(ueRestaurant.image) AND arrayLen(ueRestaurant.image) GT 0>
<cfset ueHeaderImage = ueRestaurant.image[1]>
</cfif>
<cfset menuData = {
"business": ueBusiness,
"categories": ueCategories,
"items": ueItems,
"modifiers": [],
"imageUrls": [],
"imageMappings": ueImageMappings,
"headerCandidateIndices": [],
"headerImage": ueHeaderImage
}>
<cfset response["OK"] = true>
<cfset response["DATA"] = menuData>
<cfset response["sourceUrl"] = len(targetUrl) ? targetUrl : "ubereats-upload">
<cfset response["parsedVia"] = "ubereats_jsonld">
<cfset response["pagesProcessed"] = 1>
<cfset response["imagesFound"] = structCount(ueImageMappings)>
<cfcontent type="application/json" reset="true">
<cfoutput>#serializeJSON(response)#</cfoutput>
<cfabort>
</cfif>
<cfelse>
<cfset arrayAppend(response.steps, "No Restaurant JSON-LD with menu found - falling through")>
</cfif>
<cfcatch>
<cfset ueError = "Uber Eats JSON-LD parsing failed: " & cfcatch.message>
<cfif len(cfcatch.detail)><cfset ueError = ueError & " | Detail: " & cfcatch.detail></cfif>
<cfset arrayAppend(response.steps, ueError & " - falling back to Claude")>
</cfcatch>
</cftry>
</cfif>
<!--- ========== END UBER EATS FAST PATH ========== --->
<!--- Uber Eats: No fast path — JSON-LD lacks modifiers. Falls through to Claude for full extraction. --->
<!--- Look for embedded JSON data (Next.js __NEXT_DATA__, Toast state, etc.) --->
<cfset embeddedJsonData = "">

View file

@ -167,8 +167,7 @@
i.Name AS ItemName,
i.ParentItemID,
i.Price AS ItemPrice,
i.IsCheckedByDefault,
i.IsInvertedGroup
i.IsCheckedByDefault
FROM OrderLineItems oli
INNER JOIN Items i ON i.ID = oli.ItemID
WHERE oli.OrderID = ?
@ -187,39 +186,10 @@
"ItemPrice": qLineItems.LineItemPrice,
"Quantity": qLineItems.Quantity,
"Remark": qLineItems.Remark,
"IsModifier": qLineItems.ParentOrderLineItemID GT 0,
"IsCheckedByDefault": qLineItems.IsCheckedByDefault,
"IsInvertedGroup": qLineItems.IsInvertedGroup
"IsModifier": qLineItems.ParentOrderLineItemID GT 0
})>
</cfloop>
<!--- For inverted modifier groups, compute removed defaults --->
<cfloop array="#result.LineItems#" index="li">
<cfif li.IsInvertedGroup>
<cfset qRemovedDefaults = queryExecute("
SELECT i.Name
FROM Items i
WHERE i.ParentItemID = ?
AND i.IsActive = 1
AND i.IsCheckedByDefault = b'1'
AND i.ID NOT IN (
SELECT oli2.ItemID FROM OrderLineItems oli2
WHERE oli2.OrderID = ? AND oli2.ParentOrderLineItemID = ? AND oli2.IsDeleted = b'0'
)
ORDER BY i.SortOrder
", [
{ value = li.ItemID, cfsqltype = "cf_sql_integer" },
{ value = qTask.OrderID, cfsqltype = "cf_sql_integer" },
{ value = li.LineItemID, cfsqltype = "cf_sql_integer" }
], { datasource = "payfrit" })>
<cfset removedNames = []>
<cfloop query="qRemovedDefaults">
<cfset arrayAppend(removedNames, qRemovedDefaults.Name)>
</cfloop>
<cfset li["RemovedDefaults"] = removedNames>
</cfif>
</cfloop>
<!--- Calculate order total: subtotal + tax + tip + delivery (if delivery) + platform fee --->
<cfset taxAmount = subtotal * val(qTask.TaxRate)>
<cfset tipAmount = val(qTask.TipAmount)>

View file

@ -413,23 +413,6 @@ function renderAllModifiers(modifiers, allItems) {
const leafModifiers = [];
function collectLeafModifiers(mods, depth = 0) {
mods.forEach(mod => {
// Inverted groups: show removed defaults with "NO" prefix instead of listing all selected defaults
// Check both the item itself (if group header is in order) and proxy (first child carries the data)
const isInverted = mod.IsInvertedGroup || mod.ISINVERTEDGROUP || mod.IsInvertedGroupProxy || mod.ISINVERTEDGROUPPROXY;
if (isInverted) {
const removed = mod.RemovedDefaults || mod.REMOVEDDEFAULTS || [];
if (removed.length > 0) {
const groupName = mod.ItemParentName || mod.Name;
removed.forEach(name => {
leafModifiers.push({ mod: { Name: 'NO ' + name, ItemParentName: groupName }, path: [] });
});
}
return;
}
// Skip default modifiers inside inverted groups — handled above with "NO" prefix
if (mod.IsCheckedByDefault && (mod.ParentIsInvertedGroup || mod.PARENTISINVERTEDGROUP)) return;
const children = allItems.filter(item => item.ParentOrderLineItemID === mod.OrderLineItemID);
if (children.length === 0) {
const path = getModifierPath(mod);

View file

@ -2419,7 +2419,6 @@
sortOrder: obj.sortOrder || 0,
requiresSelection: obj.requiresSelection || false,
maxSelections: obj.maxSelections || 0,
isInverted: obj.isInverted || false,
options: []
};

View file

@ -856,7 +856,7 @@
<p style="margin:0 0 2px;"><strong style="color:var(--gray-500);">Toast</strong> — full menu with modifiers &amp; images (URL or saved page)</p>
<p style="margin:0 0 2px;"><strong style="color:var(--gray-500);">Grubhub</strong> — full menu with modifiers (URL only)</p>
<p style="margin:0 0 2px;"><strong style="color:var(--gray-500);">DoorDash / order.online</strong> — items, categories, prices (URL or saved page)</p>
<p style="margin:0 0 2px;"><strong style="color:var(--gray-500);">Uber Eats</strong>items, categories, prices (URL or saved page)</p>
<p style="margin:0 0 2px;"><strong style="color:var(--gray-500);">Uber Eats</strong>full menu with modifiers (saved page)</p>
<p style="margin:0;"><strong style="color:var(--gray-500);">Other sites</strong> — upload saved page, images, or PDFs for AI extraction</p>
</div>