Add Toast modifier extraction via Playwright

When analyzing Toast menu pages, items with modifiers now have their
modifier groups extracted by clicking each item in a headless browser
and intercepting the GraphQL MenuItemDetails responses. Extracted
modifiers include group name, required/optional flag, min/max selections,
and option names with prices. Items sharing the same itemGroupGuid
inherit modifiers from successfully mapped siblings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-03-01 17:48:48 -08:00
parent 30dd0997b9
commit aca3ba18a1

View file

@ -1034,6 +1034,9 @@
<cfset itemStruct["name"] = trim(item.name)> <cfset itemStruct["name"] = trim(item.name)>
<cfset itemStruct["category"] = groupName> <cfset itemStruct["category"] = groupName>
<cfset itemStruct["modifiers"] = arrayNew(1)> <cfset itemStruct["modifiers"] = arrayNew(1)>
<cfset itemStruct["hasModifiers"] = structKeyExists(item, "hasModifiers") AND item.hasModifiers EQ true>
<cfset itemStruct["guid"] = structKeyExists(item, "guid") ? item.guid : "">
<cfset itemStruct["itemGroupGuid"] = structKeyExists(item, "itemGroupGuid") ? item.itemGroupGuid : "">
<cfset itemStruct["description"] = ""> <cfset itemStruct["description"] = "">
<cfif structKeyExists(item, "description") AND NOT isNull(item.description)> <cfif structKeyExists(item, "description") AND NOT isNull(item.description)>
<cfset itemStruct["description"] = trim(toString(item.description))> <cfset itemStruct["description"] = trim(toString(item.description))>
@ -1101,6 +1104,9 @@
<cfset itemStruct["name"] = trim(subItem.name)> <cfset itemStruct["name"] = trim(subItem.name)>
<cfset itemStruct["category"] = subName> <cfset itemStruct["category"] = subName>
<cfset itemStruct["modifiers"] = arrayNew(1)> <cfset itemStruct["modifiers"] = arrayNew(1)>
<cfset itemStruct["hasModifiers"] = structKeyExists(subItem, "hasModifiers") AND subItem.hasModifiers EQ true>
<cfset itemStruct["guid"] = structKeyExists(subItem, "guid") ? subItem.guid : "">
<cfset itemStruct["itemGroupGuid"] = structKeyExists(subItem, "itemGroupGuid") ? subItem.itemGroupGuid : "">
<cfset itemStruct["description"] = ""> <cfset itemStruct["description"] = "">
<cfif structKeyExists(subItem, "description") AND NOT isNull(subItem.description)> <cfif structKeyExists(subItem, "description") AND NOT isNull(subItem.description)>
<cfset itemStruct["description"] = trim(toString(subItem.description))> <cfset itemStruct["description"] = trim(toString(subItem.description))>
@ -1184,13 +1190,89 @@
<cfset arrayAppend(response.steps, "Extracted " & arrayLen(toastItems) & " items from " & arrayLen(toastCategories) & " categories via __OO_STATE__")> <cfset arrayAppend(response.steps, "Extracted " & arrayLen(toastItems) & " items from " & arrayLen(toastCategories) & " categories via __OO_STATE__")>
<!--- Extract Toast modifiers via Playwright if items have modifiers --->
<cfset toastModifiers = arrayNew(1)>
<cfset modifierItemCount = 0>
<cfloop array="#toastItems#" index="ti">
<cfif structKeyExists(ti, "hasModifiers") AND ti.hasModifiers>
<cfset modifierItemCount++>
</cfif>
</cfloop>
<cfif modifierItemCount GT 0>
<cfset arrayAppend(response.steps, modifierItemCount & " items have modifiers - extracting via Playwright")>
<cftry>
<!--- Determine Toast URL for Playwright --->
<cfset toastUrl = "">
<cfif isDefined("targetUrl") AND reFindNoCase("order\.toasttab\.com", targetUrl)>
<!--- URL mode: use original URL --->
<cfset toastUrl = targetUrl>
<cfelse>
<!--- Saved HTML mode: extract slug from HTML --->
<!--- Try __APOLLO_STATE__ shortUrl first --->
<cfset slugMatch = reMatchNoCase('"shortUrl"\s*:\s*"([^"]+)"', pageHtml)>
<cfif arrayLen(slugMatch)>
<cfset slug = reReplaceNoCase(slugMatch[1], '.*"shortUrl"\s*:\s*"([^"]+)".*', '\1')>
<cfset toastUrl = "https://order.toasttab.com/online/" & slug>
</cfif>
<!--- Try gift card URL pattern --->
<cfif NOT len(toastUrl)>
<cfset giftMatch = reMatchNoCase('toasttab\.com/([a-zA-Z0-9_-]+)/giftcards', pageHtml)>
<cfif arrayLen(giftMatch)>
<cfset slug = reReplaceNoCase(giftMatch[1], '.*toasttab\.com/([a-zA-Z0-9_-]+)/giftcards.*', '\1')>
<cfset toastUrl = "https://order.toasttab.com/online/" & slug>
</cfif>
</cfif>
</cfif>
<cfif len(toastUrl)>
<cfset arrayAppend(response.steps, "Fetching modifiers from: " & toastUrl)>
<cfset modOutput = "">
<cfexecute name="/opt/playwright/run-toast-modifiers.sh" arguments="'#toastUrl#'" timeout="180" variable="modOutput" />
<cfif len(trim(modOutput))>
<cfset modResult = deserializeJSON(modOutput)>
<!--- Extract modifiers --->
<cfif structKeyExists(modResult, "modifiers") AND isArray(modResult.modifiers)>
<cfset toastModifiers = modResult.modifiers>
<cfset arrayAppend(response.steps, "Extracted " & arrayLen(toastModifiers) & " unique modifier groups")>
</cfif>
<!--- Map modifiers to items --->
<cfif structKeyExists(modResult, "itemModifierMap") AND isStruct(modResult.itemModifierMap)>
<cfset modMap = modResult.itemModifierMap>
<cfloop from="1" to="#arrayLen(toastItems)#" index="mi">
<cfif structKeyExists(modMap, toastItems[mi].name)>
<cfset toastItems[mi]["modifiers"] = modMap[toastItems[mi].name]>
</cfif>
</cfloop>
<cfset arrayAppend(response.steps, "Mapped modifiers to " & structCount(modMap) & " items")>
</cfif>
<!--- Log stats --->
<cfif structKeyExists(modResult, "stats") AND isStruct(modResult.stats)>
<cfset arrayAppend(response.steps, "Modifier stats: " & serializeJSON(modResult.stats))>
</cfif>
<cfelse>
<cfset arrayAppend(response.steps, "Playwright modifier script returned empty output")>
</cfif>
<cfelse>
<cfset arrayAppend(response.steps, "Could not determine Toast URL for modifier extraction")>
</cfif>
<cfcatch>
<cfset arrayAppend(response.steps, "Modifier extraction failed: " & cfcatch.message & " - continuing without modifiers")>
</cfcatch>
</cftry>
</cfif>
<!--- Build and return response directly - skip Claude ---> <!--- Build and return response directly - skip Claude --->
<cfif arrayLen(toastItems) GT 0> <cfif arrayLen(toastItems) GT 0>
<cfset menuData = structNew()> <cfset menuData = structNew()>
<cfset menuData["business"] = toastBusiness> <cfset menuData["business"] = toastBusiness>
<cfset menuData["categories"] = toastCategories> <cfset menuData["categories"] = toastCategories>
<cfset menuData["items"] = toastItems> <cfset menuData["items"] = toastItems>
<cfset menuData["modifiers"] = arrayNew(1)> <cfset menuData["modifiers"] = toastModifiers>
<cfset menuData["imageUrls"] = arrayNew(1)> <cfset menuData["imageUrls"] = arrayNew(1)>
<cfset menuData["imageMappings"] = imageMappings> <cfset menuData["imageMappings"] = imageMappings>
<cfset menuData["headerCandidateIndices"] = arrayNew(1)> <cfset menuData["headerCandidateIndices"] = arrayNew(1)>