Enhance SandwichReminder-AutoOrder feature with direct checkout navigation and improved logic

- Added new configuration options for direct checkout navigation, including `useDirectCheckoutNavigation`, `checkoutPath`, and `checkoutOpenDelayMs`.
- Updated the auto-order flow to navigate directly to the checkout page, skipping the mini-cart and date/time steps.
- Improved keyboard automation logic for item remark and order confirmation processes.
- Removed the old SandwichAutoOrder.ps1 file as its functionality has been integrated into SandwichReminder-AutoOrder.ps1.
- Introduced a new runner-launcher.vbs to start the runner.ps1 without a visible console window, ensuring WinForms dialogs can appear.
- Implemented a mutex mechanism in runner.ps1 to prevent concurrent execution of the same feature.
This commit is contained in:
Arne Moerman
2026-05-11 13:59:22 +02:00
parent 2592c0145f
commit 6c10d359d2
7 changed files with 525 additions and 205 deletions
@@ -100,6 +100,27 @@ $FeatureMeta = @{
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'
@@ -134,10 +155,407 @@ $FeatureMeta = @{
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,