$FeatureMeta = @{ Name = 'SandwichReminder-AutoOrder' Description = 'Personal automation settings for sandwich auto-order browser flow' Settings = @( @{ Key = 'baseUrl' Label = 'Base URL' Type = 'string' Default = 'https://ylos-kitchen.unipage.eu' Description = 'Base URL of the ordering website' }, @{ Key = 'itemId' Label = 'Item ID' Type = 'string' Default = 'dmwmo3' Description = 'Item ID for direct item modal URL (default = Smoske)' }, @{ Key = 'windowTitleHint' Label = 'Browser Window Title Hint' Type = 'string' Default = "YLO'S Kitchen" Description = 'Used to bring browser window to foreground before sending keys' }, @{ Key = 'defaultItemRemark' Label = 'Default Item Remark' Type = 'string' Default = 'zonder tomaat aub' Description = 'For next step (checkout automation)' }, @{ Key = 'defaultOrderRemark' Label = 'Default Order Remark' Type = 'string' Default = 'levering in Sioen Bistro' Description = 'For next step (checkout automation)' }, @{ Key = 'initialDelayMs' Label = 'Initial Delay (ms)' Type = 'int' Default = 2500 Description = 'Wait time after opening browser before key automation starts' }, @{ Key = 'activationTimeoutMs' Label = 'Activation Timeout (ms)' Type = 'int' Default = 10000 Description = 'How long to retry foregrounding the browser window' }, @{ Key = 'stepDelayMs' Label = 'Key Step Delay (ms)' Type = 'int' Default = 120 Description = 'Delay between keyboard steps' }, @{ Key = 'optionToggleKey' Label = 'Option Toggle Key' Type = 'string' Default = 'SPACE' Description = 'Key used to toggle option checkbox: SPACE or ENTER' }, @{ Key = 'calibrationOnly' Label = 'Calibration Only' Type = 'bool' Default = $false Description = 'If enabled, only tabs through the modal and stops (no clicks/add)' }, @{ Key = 'calibrationTabs' Label = 'Calibration Tabs' Type = 'int' Default = 0 Description = 'Number of tabs to send in calibration mode before stopping' }, @{ Key = 'tabsToOption1' Label = 'Tabs To Option 1' Type = 'int' Default = 7 Description = 'Tab count from modal start to first option checkbox' }, @{ Key = 'tabsBetweenOptions' Label = 'Tabs Between Options' Type = 'int' Default = 2 Description = 'Tab count from first checkbox to second checkbox' }, @{ Key = 'tabsToAddButton' Label = 'Tabs To Add Button' Type = 'int' Default = 6 Description = 'Tab count from second checkbox to add-to-cart button' }, @{ Key = 'useDirectCheckoutNavigation' Label = 'Use Direct Checkout Navigation' Type = 'bool' Default = $true Description = 'After add-to-cart, navigate directly to /checkout to skip mini-cart and date/time steps' }, @{ Key = 'checkoutPath' Label = 'Checkout Path' Type = 'string' Default = '/checkout' Description = 'Relative path used for direct checkout navigation' }, @{ Key = 'checkoutOpenDelayMs' Label = 'Checkout Open Delay (ms)' Type = 'int' Default = 1800 Description = 'Wait time after opening checkout page before continuing keyboard flow' }, @{ Key = 'openCartPopupAfterAdd' Label = 'Open Cart Popup After Add' Type = 'bool' Default = $true Description = 'Open mini-cart after adding item' }, @{ Key = 'refreshBeforeCart' Label = 'Refresh Before Cart' Type = 'bool' Default = $true Description = 'Refresh the page after add-to-cart to reset focus to page start before tabbing to mini-cart' }, @{ Key = 'postRefreshDelayMs' Label = 'Post-Refresh Delay (ms)' Type = 'int' Default = 2500 Description = 'Wait time after F5 refresh before tabbing to mini-cart' }, @{ Key = 'tabsToCartButton' Label = 'Tabs To Cart Button' Type = 'int' Default = 5 Description = 'Tab count from page start (after refresh) to mini-cart button' }, @{ Key = 'tabsToConfirmButton' Label = 'Tabs To Confirm Button' Type = 'int' Default = 3 Description = 'Tab count from mini-cart button to confirm order button (opens delivery popup)' }, @{ Key = 'tabsToDateTimeConfirm' Label = 'Tabs To Date Time Confirm' Type = 'int' Default = 4 Description = 'Tab count on delivery popup to confirm prefilled day and time' }, @{ Key = 'tabsToItemRemarkButton' Label = 'Tabs To Item Remark Button' Type = 'int' Default = 8 Description = 'Tab count on order overview to open item remark popup' }, @{ Key = 'tabsToCloseItemRemarkPopup' Label = 'Tabs To Close Item Remark Popup' Type = 'int' Default = 1 Description = 'Tab count after filling item remark to reach confirm button in popup' }, @{ Key = 'tabsBeforeItemRemarkInput' Label = 'Tabs Before Item Remark Input' Type = 'int' Default = -5 Description = 'Tab count inside item remark popup before typing (negative means SHIFT+TAB)' }, @{ Key = 'tabsToOrderRemarkInput' Label = 'Tabs To Order Remark Input' Type = 'int' Default = 6 Description = 'Tab count from item remark popup close to order remark input' }, @{ Key = 'tabsToFinalConfirmButton' Label = 'Tabs To Final Confirm Button' Type = 'int' Default = 3 Description = 'Tab count from order remark input to final confirm button' }, @{ Key = 'tabsToPaymentOption' Label = 'Tabs To Payment Option' Type = 'int' Default = -9 Description = 'Tab count on payment option screen to default option (negative means SHIFT+TAB)' }, @{ Key = 'transitionDelayMs' Label = 'Transition Delay (ms)' Type = 'int' Default = 400 Description = 'Wait time after major Enter actions while screens or popups render' }, @{ Key = 'deliveryPopupRenderDelayMs' Label = 'Delivery Popup Render Delay (ms)' Type = 'int' Default = 1200 Description = 'Extra wait right before the 4-tab date/time confirm step' }, @{ Key = 'checkoutOverviewRenderDelayMs' Label = 'Checkout Overview Render Delay (ms)' Type = 'int' Default = 1200 Description = 'Extra wait after confirming date/time before running 3-tab item remark navigation' }, @{ Key = 'itemRemarkPopupRenderDelayMs' Label = 'Item Remark Popup Render Delay (ms)' Type = 'int' Default = 1200 Description = 'Extra wait after opening item remark popup before tabbing and typing' } ) } function Invoke-AppActivateBestEffort { param( [Parameter(Mandatory)]$Shell, [Parameter(Mandatory)][string[]]$TitleCandidates, [int]$TimeoutMs = 10000, [int]$RetryDelayMs = 300 ) $deadline = (Get-Date).AddMilliseconds($TimeoutMs) $candidates = @($TitleCandidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) do { foreach ($candidate in $candidates) { if ($Shell.AppActivate($candidate)) { return $true } } Start-Sleep -Milliseconds $RetryDelayMs } while ((Get-Date) -lt $deadline) return $false } function ConvertTo-SendKeysSafeText { param( [string]$Text ) if ($null -eq $Text) { return '' } # Escape SendKeys reserved characters so user remarks are typed literally. $escaped = $Text $escaped = $escaped -replace '(\+|\^|%|~|\(|\)|\[|\]|\{|\})', '{$1}' return $escaped } function Send-TabSteps { param( [Parameter(Mandatory)]$Shell, [int]$Count, [int]$StepDelayMs ) $key = if ($Count -lt 0) { '+{TAB}' } else { '{TAB}' } $iterations = [Math]::Abs($Count) for ($i = 0; $i -lt $iterations; $i++) { $Shell.SendKeys($key) Start-Sleep -Milliseconds $StepDelayMs } } function Invoke-SandwichAutoOrderFlow { [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$AutoOrderConfig, [Parameter(Mandatory)][string]$FallbackUrl, [string]$FeatureName = 'SandwichReminder-AutoOrder' ) $baseUrl = [string]$AutoOrderConfig['baseUrl'] if ([string]::IsNullOrWhiteSpace($baseUrl)) { $baseUrl = 'https://ylos-kitchen.unipage.eu' } $itemId = [string]$AutoOrderConfig['itemId'] if ([string]::IsNullOrWhiteSpace($itemId)) { $itemId = 'dmwmo3' } $windowTitleHint = [string]$AutoOrderConfig['windowTitleHint'] if ([string]::IsNullOrWhiteSpace($windowTitleHint)) { $windowTitleHint = "YLO'S Kitchen" } $initialDelayMs = [int]$AutoOrderConfig['initialDelayMs'] if ($initialDelayMs -le 0) { $initialDelayMs = 2500 } $stepDelayMs = [int]$AutoOrderConfig['stepDelayMs'] if ($stepDelayMs -le 0) { $stepDelayMs = 120 } $toggleKeySetting = [string]$AutoOrderConfig['optionToggleKey'] if ([string]::IsNullOrWhiteSpace($toggleKeySetting)) { $toggleKeySetting = 'SPACE' } $toggleKey = if ($toggleKeySetting.ToUpperInvariant() -eq 'ENTER') { '{ENTER}' } else { ' ' } $calibrationOnly = [bool]$AutoOrderConfig['calibrationOnly'] $calibrationTabs = [int]$AutoOrderConfig['calibrationTabs'] if ($calibrationTabs -lt 0) { $calibrationTabs = 0 } $tabsToOption1 = [int]$AutoOrderConfig['tabsToOption1'] if ($tabsToOption1 -lt 0) { $tabsToOption1 = 3 } $tabsBetweenOptions = [int]$AutoOrderConfig['tabsBetweenOptions'] if ($tabsBetweenOptions -lt 0) { $tabsBetweenOptions = 2 } $tabsToAddButton = [int]$AutoOrderConfig['tabsToAddButton'] if ($tabsToAddButton -lt 0) { $tabsToAddButton = 4 } $useDirectCheckoutNavigation = [bool]$AutoOrderConfig['useDirectCheckoutNavigation'] $checkoutPath = [string]$AutoOrderConfig['checkoutPath'] if ([string]::IsNullOrWhiteSpace($checkoutPath)) { $checkoutPath = '/checkout' } if (-not $checkoutPath.StartsWith('/')) { $checkoutPath = '/' + $checkoutPath } $checkoutOpenDelayMs = [int]$AutoOrderConfig['checkoutOpenDelayMs'] if ($checkoutOpenDelayMs -le 0) { $checkoutOpenDelayMs = 1800 } $openCartPopup = [bool]$AutoOrderConfig['openCartPopupAfterAdd'] $refreshBeforeCart = [bool]$AutoOrderConfig['refreshBeforeCart'] $postRefreshDelayMs = [int]$AutoOrderConfig['postRefreshDelayMs'] if ($postRefreshDelayMs -le 0) { $postRefreshDelayMs = 2500 } $tabsToCartButton = [int]$AutoOrderConfig['tabsToCartButton'] if ($tabsToCartButton -le 0) { $tabsToCartButton = 5 } $tabsToConfirmButton = [int]$AutoOrderConfig['tabsToConfirmButton'] if ($tabsToConfirmButton -le 0) { $tabsToConfirmButton = 3 } $tabsToDateTimeConfirm = [int]$AutoOrderConfig['tabsToDateTimeConfirm'] if ($tabsToDateTimeConfirm -le 0) { $tabsToDateTimeConfirm = 4 } $tabsToItemRemarkButton = [int]$AutoOrderConfig['tabsToItemRemarkButton'] if ($tabsToItemRemarkButton -le 0) { $tabsToItemRemarkButton = 8 } $tabsToCloseItemRemarkPopup = [int]$AutoOrderConfig['tabsToCloseItemRemarkPopup'] if ($tabsToCloseItemRemarkPopup -le 0) { $tabsToCloseItemRemarkPopup = 1 } $tabsBeforeItemRemarkInput = [int]$AutoOrderConfig['tabsBeforeItemRemarkInput'] if ($tabsBeforeItemRemarkInput -eq 0) { $tabsBeforeItemRemarkInput = -5 } $tabsToOrderRemarkInput = [int]$AutoOrderConfig['tabsToOrderRemarkInput'] if ($tabsToOrderRemarkInput -le 0) { $tabsToOrderRemarkInput = 6 } $tabsToFinalConfirmButton = [int]$AutoOrderConfig['tabsToFinalConfirmButton'] if ($tabsToFinalConfirmButton -le 0) { $tabsToFinalConfirmButton = 3 } $tabsToPaymentOption = [int]$AutoOrderConfig['tabsToPaymentOption'] if ($tabsToPaymentOption -eq 0) { $tabsToPaymentOption = -9 } $transitionDelayMs = [int]$AutoOrderConfig['transitionDelayMs'] if ($transitionDelayMs -le 0) { $transitionDelayMs = 400 } $deliveryPopupRenderDelayMs = [int]$AutoOrderConfig['deliveryPopupRenderDelayMs'] if ($deliveryPopupRenderDelayMs -le 0) { $deliveryPopupRenderDelayMs = 1200 } $checkoutOverviewRenderDelayMs = [int]$AutoOrderConfig['checkoutOverviewRenderDelayMs'] if ($checkoutOverviewRenderDelayMs -le 0) { $checkoutOverviewRenderDelayMs = 1200 } $itemRemarkPopupRenderDelayMs = [int]$AutoOrderConfig['itemRemarkPopupRenderDelayMs'] if ($itemRemarkPopupRenderDelayMs -le 0) { $itemRemarkPopupRenderDelayMs = 1200 } $activationTimeoutMs = [int]$AutoOrderConfig['activationTimeoutMs'] if ($activationTimeoutMs -le 0) { $activationTimeoutMs = 10000 } $itemUrl = ($baseUrl.TrimEnd('/') + '/item/' + $itemId) $checkoutUrl = ($baseUrl.TrimEnd('/') + $checkoutPath) try { Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null Start-Process $itemUrl Write-Log -Level Info -Message "Opened item modal URL for auto-order: $itemUrl" -Feature $FeatureName Start-Sleep -Milliseconds $initialDelayMs $shell = New-Object -ComObject WScript.Shell $hostHint = $null try { $hostHint = ([uri]$baseUrl).Host } catch {} $titleCandidates = @( 'Bestel online bij YLO''S Kitchen Pittem', 'Bestel online bij YLO', 'Bestel online bij', $windowTitleHint, ($windowTitleHint -replace "'", ''), $hostHint, 'ylos-kitchen', 'unipage' ) $activated = Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs $activationTimeoutMs -RetryDelayMs 300 if (-not $activated) { Write-Log -Level Warn -Message "Could not foreground browser window (title hint '$windowTitleHint'). Skipping key automation to avoid acting on the wrong window." -Feature $FeatureName return $false } if ($calibrationOnly) { for ($i = 0; $i -lt $calibrationTabs; $i++) { $shell.SendKeys('{TAB}') Start-Sleep -Milliseconds $stepDelayMs } Write-Log -Level Info -Message ("Calibration mode completed: sent {0} TAB keys and stopped." -f $calibrationTabs) -Feature $FeatureName return $true } # Modal flow keyboard navigation (best-effort; tab indexes are configurable). for ($i = 0; $i -lt $tabsToOption1; $i++) { $shell.SendKeys('{TAB}') Start-Sleep -Milliseconds $stepDelayMs } $shell.SendKeys($toggleKey) Start-Sleep -Milliseconds $stepDelayMs for ($i = 0; $i -lt $tabsBetweenOptions; $i++) { $shell.SendKeys('{TAB}') Start-Sleep -Milliseconds $stepDelayMs } $shell.SendKeys($toggleKey) Start-Sleep -Milliseconds $stepDelayMs for ($i = 0; $i -lt $tabsToAddButton; $i++) { $shell.SendKeys('{TAB}') Start-Sleep -Milliseconds $stepDelayMs } $shell.SendKeys('{ENTER}') Start-Sleep -Milliseconds 350 if ($useDirectCheckoutNavigation) { Start-Process $checkoutUrl Write-Log -Level Info -Message "Opened checkout URL directly: $checkoutUrl" -Feature $FeatureName Start-Sleep -Milliseconds $checkoutOpenDelayMs $activated = Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs $activationTimeoutMs -RetryDelayMs 300 if (-not $activated) { Write-Log -Level Warn -Message 'Could not foreground browser after opening checkout URL. Skipping remaining checkout automation.' -Feature $FeatureName return $false } # Order overview: open item remark popup. Start-Sleep -Milliseconds $checkoutOverviewRenderDelayMs [void](Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs 4000 -RetryDelayMs 200) Send-TabSteps -Shell $shell -Count $tabsToItemRemarkButton -StepDelayMs $stepDelayMs $shell.SendKeys('{ENTER}') # Item remark popup: type remark and confirm. Start-Sleep -Milliseconds $itemRemarkPopupRenderDelayMs [void](Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs 4000 -RetryDelayMs 200) Send-TabSteps -Shell $shell -Count $tabsBeforeItemRemarkInput -StepDelayMs $stepDelayMs $itemRemark = ConvertTo-SendKeysSafeText -Text ([string]$AutoOrderConfig['defaultItemRemark']) if ([string]::IsNullOrWhiteSpace($itemRemark)) { $itemRemark = 'zonder tomaat aub' } $shell.SendKeys($itemRemark) Start-Sleep -Milliseconds $stepDelayMs Send-TabSteps -Shell $shell -Count $tabsToCloseItemRemarkPopup -StepDelayMs $stepDelayMs $shell.SendKeys('{ENTER}') # Back on order overview: move to order remark input, type text, confirm order. Start-Sleep -Milliseconds $transitionDelayMs Send-TabSteps -Shell $shell -Count $tabsToOrderRemarkInput -StepDelayMs $stepDelayMs $orderRemark = ConvertTo-SendKeysSafeText -Text ([string]$AutoOrderConfig['defaultOrderRemark']) if ([string]::IsNullOrWhiteSpace($orderRemark)) { $orderRemark = 'levering in Sioen Bistro' } $shell.SendKeys($orderRemark) Start-Sleep -Milliseconds $stepDelayMs Send-TabSteps -Shell $shell -Count $tabsToFinalConfirmButton -StepDelayMs $stepDelayMs $shell.SendKeys('{ENTER}') # Payment option screen: move to default option and confirm, then handoff to user. Start-Sleep -Milliseconds $transitionDelayMs Send-TabSteps -Shell $shell -Count $tabsToPaymentOption -StepDelayMs $stepDelayMs $shell.SendKeys('{ENTER}') Write-Log -Level Info -Message 'Auto-order flow executed through direct checkout navigation and payment option confirmation; handoff to user at payment screen (best-effort).' -Feature $FeatureName return $true } if ($openCartPopup) { if ($refreshBeforeCart) { $shell.SendKeys('{F5}') Write-Log -Level Info -Message 'Refreshing page to reset focus before tabbing to mini-cart.' -Feature $FeatureName Start-Sleep -Milliseconds $postRefreshDelayMs $activated = Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs $activationTimeoutMs -RetryDelayMs 300 if (-not $activated) { Write-Log -Level Warn -Message 'Could not re-foreground browser after refresh. Skipping cart popup.' -Feature $FeatureName return $false } } for ($i = 0; $i -lt $tabsToCartButton; $i++) { $shell.SendKeys('{TAB}') Start-Sleep -Milliseconds $stepDelayMs } $shell.SendKeys('{ENTER}') Start-Sleep -Milliseconds $stepDelayMs for ($i = 0; $i -lt $tabsToConfirmButton; $i++) { $shell.SendKeys('{TAB}') Start-Sleep -Milliseconds $stepDelayMs } $shell.SendKeys('{ENTER}') # Delivery popup: re-activate and wait longer before 4-tab date/time confirm. Start-Sleep -Milliseconds $deliveryPopupRenderDelayMs [void](Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs 4000 -RetryDelayMs 200) Send-TabSteps -Shell $shell -Count $tabsToDateTimeConfirm -StepDelayMs $stepDelayMs $shell.SendKeys('{ENTER}') # Order overview: open item remark popup. Start-Sleep -Milliseconds $checkoutOverviewRenderDelayMs [void](Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs 4000 -RetryDelayMs 200) Send-TabSteps -Shell $shell -Count $tabsToItemRemarkButton -StepDelayMs $stepDelayMs $shell.SendKeys('{ENTER}') # Item remark popup: type remark and confirm. Start-Sleep -Milliseconds $itemRemarkPopupRenderDelayMs [void](Invoke-AppActivateBestEffort -Shell $shell -TitleCandidates $titleCandidates -TimeoutMs 4000 -RetryDelayMs 200) Send-TabSteps -Shell $shell -Count $tabsBeforeItemRemarkInput -StepDelayMs $stepDelayMs $itemRemark = ConvertTo-SendKeysSafeText -Text ([string]$AutoOrderConfig['defaultItemRemark']) if ([string]::IsNullOrWhiteSpace($itemRemark)) { $itemRemark = 'zonder tomaat aub' } $shell.SendKeys($itemRemark) Start-Sleep -Milliseconds $stepDelayMs Send-TabSteps -Shell $shell -Count $tabsToCloseItemRemarkPopup -StepDelayMs $stepDelayMs $shell.SendKeys('{ENTER}') # Back on order overview: move to order remark input, type text, confirm order. Start-Sleep -Milliseconds $transitionDelayMs Send-TabSteps -Shell $shell -Count $tabsToOrderRemarkInput -StepDelayMs $stepDelayMs $orderRemark = ConvertTo-SendKeysSafeText -Text ([string]$AutoOrderConfig['defaultOrderRemark']) if ([string]::IsNullOrWhiteSpace($orderRemark)) { $orderRemark = 'levering in Sioen Bistro' } $shell.SendKeys($orderRemark) Start-Sleep -Milliseconds $stepDelayMs Send-TabSteps -Shell $shell -Count $tabsToFinalConfirmButton -StepDelayMs $stepDelayMs $shell.SendKeys('{ENTER}') # Payment option screen: move to default option and confirm, then handoff to user. Start-Sleep -Milliseconds $transitionDelayMs Send-TabSteps -Shell $shell -Count $tabsToPaymentOption -StepDelayMs $stepDelayMs $shell.SendKeys('{ENTER}') } Write-Log -Level Info -Message 'Auto-order flow executed through payment option confirmation; handoff to user at payment screen (best-effort).' -Feature $FeatureName return $true } catch { Write-Log -Level Error -Message "Auto-order flow failed: $_" -Feature $FeatureName return $false } } function Invoke-Feature { param( [hashtable]$Config, [hashtable]$State ) if (-not $State) { $State = @{} } return $State }