Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1beca924fc | ||
|
|
cea469c95a |
@@ -164,7 +164,30 @@ jobs:
|
||||
| docker compose -f docker-compose.yml -f .github/docker-compose.ci.yml exec -T php1 tar -C /var/www/html -xf -
|
||||
|
||||
- name: Resolve dependencies
|
||||
run: docker compose -f docker-compose.yml -f .github/docker-compose.ci.yml exec -T php1 sh -lc "cd /var/www/html && composer install --no-interaction --prefer-dist --no-progress"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
composer_install() {
|
||||
install_mode="$1"
|
||||
max_attempts="$2"
|
||||
attempt=1
|
||||
while :; do
|
||||
if docker compose -f docker-compose.yml -f .github/docker-compose.ci.yml exec -T php1 sh -lc "cd /var/www/html && composer install --no-interaction ${install_mode} --no-progress"; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$attempt" -ge "$max_attempts" ]; then
|
||||
return 1
|
||||
fi
|
||||
sleep_seconds=$((attempt * 5))
|
||||
echo "composer install ${install_mode} failed; retrying in ${sleep_seconds}s (attempt $((attempt + 1))/${max_attempts})" >&2
|
||||
sleep "$sleep_seconds"
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
}
|
||||
|
||||
composer_install --prefer-dist 3 || {
|
||||
echo "Composer dist install failed; retrying with --prefer-source." >&2
|
||||
composer_install --prefer-source 2
|
||||
}
|
||||
|
||||
- name: Verify edge gateway test files
|
||||
run: >
|
||||
|
||||
@@ -9421,6 +9421,14 @@ paths:
|
||||
license_plate:
|
||||
type: string
|
||||
description: Required for START command
|
||||
wash_type:
|
||||
type: string
|
||||
enum: [Manual, Machine]
|
||||
description: Optional customer-selected wash type for START. When provided, Manual and Machine start actions use this explicit choice instead of inferring mode from allowed services.
|
||||
wash_mode:
|
||||
type: string
|
||||
enum: [manual, machine]
|
||||
description: Lowercase alias for wash_type accepted by backend clients.
|
||||
customer_number:
|
||||
type: integer
|
||||
description: Required for START and RESERVE commands. The authenticated customer's number is applied server-side when omitted by user clients.
|
||||
|
||||
+17
-2
@@ -68,6 +68,22 @@ retry_command() {
|
||||
done
|
||||
}
|
||||
|
||||
composer_install() {
|
||||
dist_attempts="${PHP_CI_COMPOSER_RETRIES:-3}"
|
||||
source_attempts="${PHP_CI_COMPOSER_SOURCE_RETRIES:-2}"
|
||||
|
||||
if retry_command "$dist_attempts" \
|
||||
docker compose $compose_files exec -T php1 sh -lc \
|
||||
'cd /var/www/html && composer install --no-interaction --prefer-dist --no-progress'; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Composer dist install failed after ${dist_attempts} attempts; retrying with --prefer-source." >&2
|
||||
retry_command "$source_attempts" \
|
||||
docker compose $compose_files exec -T php1 sh -lc \
|
||||
'cd /var/www/html && composer install --no-interaction --prefer-source --no-progress'
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
status="$?"
|
||||
collect_logs "$status"
|
||||
@@ -112,8 +128,7 @@ tar \
|
||||
-C services/nginx/app -cf - . \
|
||||
| docker compose $compose_files exec -T php1 tar -C /var/www/html -xf -
|
||||
|
||||
docker compose $compose_files exec -T php1 sh -lc \
|
||||
'cd /var/www/html && composer install --no-interaction --prefer-dist --no-progress'
|
||||
composer_install
|
||||
|
||||
docker compose $compose_files exec -T php1 sh -lc \
|
||||
"cd /var/www/html && composer test:ci:$suite"
|
||||
|
||||
@@ -7,6 +7,7 @@ class selfserve_lane_command_arguments
|
||||
public ?string $license_plate = null;
|
||||
public ?int $customer_number = null;
|
||||
public ?int $subuser_id = null;
|
||||
public ?string $wash_mode = null;
|
||||
public bool $defer_relay_side_effects = false;
|
||||
|
||||
/**
|
||||
@@ -32,6 +33,22 @@ class selfserve_lane_command_arguments
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setWashMode(?string $wash_mode): self
|
||||
{
|
||||
$normalized = strtolower(trim((string)$wash_mode));
|
||||
if ($wash_mode === null || $normalized === '') {
|
||||
$this->wash_mode = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (!in_array($normalized, ['manual', 'machine'], true)) {
|
||||
throw new \InvalidArgumentException('Invalid wash type: ' . $wash_mode);
|
||||
}
|
||||
|
||||
$this->wash_mode = $normalized;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDeferRelaySideEffects(bool $defer_relay_side_effects): self
|
||||
{
|
||||
$this->defer_relay_side_effects = $defer_relay_side_effects;
|
||||
@@ -50,6 +67,12 @@ class selfserve_lane_command_arguments
|
||||
if (array_key_exists('subuser_id', $params)) {
|
||||
$this->setSubuserId($params['subuser_id'] === null ? null : (int)$params['subuser_id']);
|
||||
}
|
||||
if (array_key_exists('wash_type', $params)) {
|
||||
$this->setWashMode($params['wash_type'] === null ? null : (string)$params['wash_type']);
|
||||
}
|
||||
if (array_key_exists('wash_mode', $params)) {
|
||||
$this->setWashMode($params['wash_mode'] === null ? null : (string)$params['wash_mode']);
|
||||
}
|
||||
if (array_key_exists('defer_relay_side_effects', $params)) {
|
||||
$this->setDeferRelaySideEffects(filter_var(
|
||||
$params['defer_relay_side_effects'],
|
||||
|
||||
@@ -171,12 +171,12 @@ trait selfserve_lane_command_t
|
||||
* Ensure machine relay is ON when a wash starts, when it is allowed by configuration.
|
||||
* If machine relay is not configured, this is a no-op.
|
||||
*/
|
||||
protected function setMachineRelayStatusForWashStart(): void
|
||||
protected function setMachineRelayStatusForWashStart(?selfserve_lane_command_arguments $arguments = null): void
|
||||
{
|
||||
if (!$this->isRelayConfigured(selfserve_lane_relay::MACHINE)) {
|
||||
return;
|
||||
}
|
||||
if ($this->isMachineWashSelectedAndAvailableForStart()) {
|
||||
if ($this->isMachineWashSelectedAndAvailableForStart($arguments)) {
|
||||
try {
|
||||
$this->setMachineRelayStatusHard(true);
|
||||
} catch (\Throwable) {
|
||||
@@ -194,38 +194,39 @@ trait selfserve_lane_command_t
|
||||
|
||||
/**
|
||||
* Keep the program picker relay aligned with the selected wash mode at START.
|
||||
* It is ON only when the active self-serve session is allowed to start machine wash.
|
||||
* It is ON only when the customer explicitly selected machine wash.
|
||||
*/
|
||||
protected function setProgramPickerRelayStatusForWashStart(): void
|
||||
protected function setProgramPickerRelayStatusForWashStart(?selfserve_lane_command_arguments $arguments = null): void
|
||||
{
|
||||
if (!$this->isRelayConfigured(selfserve_lane_relay::MACHINE_PROGRAM_PICKER)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$shouldEnable = $this->isMachineWashSelectedAndAvailableForStart();
|
||||
$shouldEnable = $this->isExplicitMachineWashModeSelectedForStart($arguments)
|
||||
&& $this->isMachineWashSelectedAndAvailableForStart($arguments);
|
||||
$this->setMachineProgramPickerRelayStatusHard($shouldEnable);
|
||||
} catch (\Throwable) {
|
||||
// Best effort only; wash start must continue.
|
||||
}
|
||||
}
|
||||
|
||||
protected function setProgramPickerRelayStatusFromSelectedServiceForWashStart(): void
|
||||
protected function setProgramPickerRelayStatusFromSelectedServiceForWashStart(?selfserve_lane_command_arguments $arguments = null): void
|
||||
{
|
||||
if (!$this->isRelayConfigured(selfserve_lane_relay::MACHINE_PROGRAM_PICKER)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->setMachineProgramPickerRelayStatusHard($this->isMachineServiceSelectedForWashStart());
|
||||
$this->setMachineProgramPickerRelayStatusHard($this->shouldEnableProgramPickerRelayForWashStart($arguments));
|
||||
} catch (\Throwable) {
|
||||
// Best effort only; wash start must continue.
|
||||
}
|
||||
}
|
||||
|
||||
protected function isMachineWashSelectedAndAvailableForStart(): bool
|
||||
protected function isMachineWashSelectedAndAvailableForStart(?selfserve_lane_command_arguments $arguments = null): bool
|
||||
{
|
||||
if (!$this->isMachineServiceSelectedForWashStart()) {
|
||||
if (!$this->shouldEnableSelectedMachineServiceForWashStart($arguments)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -248,6 +249,39 @@ trait selfserve_lane_command_t
|
||||
}
|
||||
}
|
||||
|
||||
protected function shouldEnableSelectedMachineServiceForWashStart(?selfserve_lane_command_arguments $arguments = null): bool
|
||||
{
|
||||
if (!$this->isMachineWashModeSelectedForStart($arguments)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isMachineServiceSelectedForWashStart();
|
||||
}
|
||||
|
||||
protected function shouldEnableProgramPickerRelayForWashStart(?selfserve_lane_command_arguments $arguments = null): bool
|
||||
{
|
||||
return $this->isExplicitMachineWashModeSelectedForStart($arguments)
|
||||
&& $this->isMachineServiceSelectedForWashStart();
|
||||
}
|
||||
|
||||
protected function isExplicitMachineWashModeSelectedForStart(?selfserve_lane_command_arguments $arguments = null): bool
|
||||
{
|
||||
return $arguments !== null && $arguments->wash_mode === selfserve_studio_actions::MODE_MACHINE;
|
||||
}
|
||||
|
||||
protected function isMachineWashModeSelectedForStart(?selfserve_lane_command_arguments $arguments = null): bool
|
||||
{
|
||||
if ($arguments !== null && $arguments->wash_mode === selfserve_studio_actions::MODE_MANUAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($arguments !== null && $arguments->wash_mode === selfserve_studio_actions::MODE_MACHINE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->isMachineServiceSelectedForWashStart();
|
||||
}
|
||||
|
||||
protected function isMachineServiceSelectedForWashStart(): bool
|
||||
{
|
||||
try {
|
||||
@@ -349,17 +383,24 @@ trait selfserve_lane_command_t
|
||||
protected function runRelaySideEffectsForWashStart(selfserve_lane_command_arguments $arguments): void
|
||||
{
|
||||
if ($arguments->defer_relay_side_effects) {
|
||||
$this->setProgramPickerRelayStatusFromSelectedServiceForWashStart();
|
||||
$this->setProgramPickerRelayStatusFromSelectedServiceForWashStart($arguments);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->turnOnCleanerRelayForWashStart();
|
||||
$this->setProgramPickerRelayStatusForWashStart();
|
||||
$this->setMachineRelayStatusForWashStart();
|
||||
$this->setProgramPickerRelayStatusForWashStart($arguments);
|
||||
$this->setMachineRelayStatusForWashStart($arguments);
|
||||
}
|
||||
|
||||
protected function resolveSelfServeActionWashModeForStart(): string
|
||||
protected function resolveSelfServeActionWashModeForStart(?selfserve_lane_command_arguments $arguments = null): string
|
||||
{
|
||||
if ($arguments !== null && in_array($arguments->wash_mode, [
|
||||
selfserve_studio_actions::MODE_MANUAL,
|
||||
selfserve_studio_actions::MODE_MACHINE,
|
||||
], true)) {
|
||||
return $arguments->wash_mode;
|
||||
}
|
||||
|
||||
if ($this->isMachineServiceSelectedForWashStart()) {
|
||||
return selfserve_studio_actions::MODE_MACHINE;
|
||||
}
|
||||
@@ -634,7 +675,7 @@ trait selfserve_lane_command_t
|
||||
$this->runRelaySideEffectsForWashStart($arguments);
|
||||
$this->runPublishedStudioActions(
|
||||
selfserve_studio_actions::EVENT_WASH_START_COMMAND,
|
||||
$this->resolveSelfServeActionWashModeForStart(),
|
||||
$this->resolveSelfServeActionWashModeForStart($arguments),
|
||||
[
|
||||
'customer_number' => (int)$customer_number,
|
||||
'reg' => $license_plate,
|
||||
|
||||
@@ -9421,6 +9421,14 @@ paths:
|
||||
license_plate:
|
||||
type: string
|
||||
description: Required for START command
|
||||
wash_type:
|
||||
type: string
|
||||
enum: [Manual, Machine]
|
||||
description: Optional customer-selected wash type for START. When provided, Manual and Machine start actions use this explicit choice instead of inferring mode from allowed services.
|
||||
wash_mode:
|
||||
type: string
|
||||
enum: [manual, machine]
|
||||
description: Lowercase alias for wash_type accepted by backend clients.
|
||||
customer_number:
|
||||
type: integer
|
||||
description: Required for START and RESERVE commands. The authenticated customer's number is applied server-side when omitted by user clients.
|
||||
|
||||
@@ -76,6 +76,7 @@ it('allows the customer self-serve start sequence without department access', fu
|
||||
'lane_id' => $laneId,
|
||||
'command' => 'START',
|
||||
'license_plate' => $reg,
|
||||
'wash_type' => 'Manual',
|
||||
'defer_relay_side_effects' => true,
|
||||
], $headers)
|
||||
->assertStatus(200)
|
||||
@@ -131,6 +132,7 @@ it('marks the active customer session relay-enabled after machine relay enable',
|
||||
'lane_id' => $laneId,
|
||||
'command' => 'START',
|
||||
'license_plate' => $reg,
|
||||
'wash_type' => 'Machine',
|
||||
'defer_relay_side_effects' => true,
|
||||
], $headers)
|
||||
->assertStatus(200)
|
||||
|
||||
@@ -64,6 +64,7 @@ it('allows customer self-serve permission to execute START without department ac
|
||||
'lane_id' => (int)$scenario['lane']['id'],
|
||||
'command' => 'START',
|
||||
'license_plate' => (string)$scenario['vehicle']['reg'],
|
||||
'wash_type' => 'Manual',
|
||||
'defer_relay_side_effects' => true,
|
||||
], api_fixtures()->bearerHeaders($token));
|
||||
|
||||
@@ -170,6 +171,7 @@ it('still allows elevated operators with department access to execute lane comma
|
||||
'lane_id' => (int)$scenario['lane']['id'],
|
||||
'command' => 'START',
|
||||
'license_plate' => 'OP' . (int)$scenario['lane']['id'],
|
||||
'wash_type' => 'Manual',
|
||||
'defer_relay_side_effects' => true,
|
||||
], $session['headers']);
|
||||
|
||||
|
||||
@@ -53,20 +53,23 @@ class SelfserveLaneStartEntranceTimeoutHarness
|
||||
$this->relayEvents[] = 'cleaner:on';
|
||||
}
|
||||
|
||||
protected function setProgramPickerRelayStatusFromSelectedServiceForWashStart(): void
|
||||
protected function setProgramPickerRelayStatusFromSelectedServiceForWashStart(?selfserve_lane_command_arguments $arguments = null): void
|
||||
{
|
||||
unset($arguments);
|
||||
$this->programPickerRelayCalls++;
|
||||
$this->relayEvents[] = 'program_picker:selected_service';
|
||||
}
|
||||
|
||||
protected function setProgramPickerRelayStatusForWashStart(): void
|
||||
protected function setProgramPickerRelayStatusForWashStart(?selfserve_lane_command_arguments $arguments = null): void
|
||||
{
|
||||
unset($arguments);
|
||||
$this->programPickerRelayCalls++;
|
||||
$this->relayEvents[] = 'program_picker:eligibility_sync';
|
||||
}
|
||||
|
||||
protected function setMachineRelayStatusForWashStart(): void
|
||||
protected function setMachineRelayStatusForWashStart(?selfserve_lane_command_arguments $arguments = null): void
|
||||
{
|
||||
unset($arguments);
|
||||
$this->machineRelayCalls++;
|
||||
$this->relayEvents[] = 'machine:sync';
|
||||
}
|
||||
@@ -107,14 +110,30 @@ it('parses deferred relay side effects on start command arguments', function ():
|
||||
$arguments = (new selfserve_lane_command_arguments())->setParameters([
|
||||
'license_plate' => 'ab12345',
|
||||
'customer_number' => 12345679,
|
||||
'wash_type' => 'Manual',
|
||||
'defer_relay_side_effects' => true,
|
||||
]);
|
||||
|
||||
expect($arguments->license_plate)->toBe('AB12345');
|
||||
expect($arguments->customer_number)->toBe(12345679);
|
||||
expect($arguments->wash_mode)->toBe('manual');
|
||||
expect($arguments->defer_relay_side_effects)->toBeTrue();
|
||||
});
|
||||
|
||||
it('parses wash mode aliases on start command arguments', function (): void {
|
||||
$arguments = (new selfserve_lane_command_arguments())->setParameters([
|
||||
'wash_mode' => 'machine',
|
||||
]);
|
||||
|
||||
expect($arguments->wash_mode)->toBe('machine');
|
||||
});
|
||||
|
||||
it('rejects invalid wash types on start command arguments', function (): void {
|
||||
expect(fn() => (new selfserve_lane_command_arguments())->setParameters([
|
||||
'wash_type' => 'automatic',
|
||||
]))->toThrow(\InvalidArgumentException::class, 'Invalid wash type: automatic');
|
||||
});
|
||||
|
||||
it('only syncs program picker from selected service when start asks to defer machine side effects', function (): void {
|
||||
$lane = new SelfserveLaneStartEntranceTimeoutHarness();
|
||||
|
||||
|
||||
@@ -138,6 +138,8 @@ it('documents property gate lane commands and sanitized gate failure responses',
|
||||
|
||||
expect($commandPathBlock)->toContain('OPEN_PROPERTY_ACCESS_GATE');
|
||||
expect($commandPathBlock)->toContain('OPEN_PROPERTY_EXIT_GATE');
|
||||
expect($commandPathBlock)->toContain('wash_type:');
|
||||
expect($commandPathBlock)->toContain('wash_mode:');
|
||||
expect($commandPathBlock)->toContain('Command execution failed');
|
||||
expect($commandPathBlock)->toContain('Failed to execute command: Failed to open property access gate.');
|
||||
|
||||
|
||||
+63
-6
@@ -41,6 +41,8 @@ class SelfserveProgramPickerSelectionHarness
|
||||
public object $department_lane;
|
||||
public int $licensePlateReads = 0;
|
||||
public int $customerNumberReads = 0;
|
||||
public int $availabilityChecks = 0;
|
||||
public bool $machineAvailable = true;
|
||||
/** @var array<int,bool> */
|
||||
public array $programPickerWrites = [];
|
||||
/** @var array<string,mixed> */
|
||||
@@ -79,29 +81,84 @@ class SelfserveProgramPickerSelectionHarness
|
||||
return true;
|
||||
}
|
||||
|
||||
public function runDeferredStartRelaySideEffects(): void
|
||||
public function runDeferredStartRelaySideEffects(?string $washType = null): void
|
||||
{
|
||||
$arguments = (new selfserve_lane_command_arguments())->setDeferRelaySideEffects(true);
|
||||
$arguments = (new selfserve_lane_command_arguments())
|
||||
->setDeferRelaySideEffects(true)
|
||||
->setWashMode($washType);
|
||||
$this->runRelaySideEffectsForWashStart($arguments);
|
||||
}
|
||||
|
||||
public function runNormalStartProgramPickerRelay(?string $washType = null): void
|
||||
{
|
||||
$arguments = (new selfserve_lane_command_arguments())->setWashMode($washType);
|
||||
$this->setProgramPickerRelayStatusForWashStart($arguments);
|
||||
}
|
||||
|
||||
public function resolveStartWashMode(?string $washType = null): string
|
||||
{
|
||||
$arguments = (new selfserve_lane_command_arguments())->setWashMode($washType);
|
||||
return $this->resolveSelfServeActionWashModeForStart($arguments);
|
||||
}
|
||||
|
||||
protected function isMachineWashSelectedAndAvailableForStart(?selfserve_lane_command_arguments $arguments = null): bool
|
||||
{
|
||||
unset($arguments);
|
||||
$this->availabilityChecks++;
|
||||
return $this->machineAvailable;
|
||||
}
|
||||
}
|
||||
|
||||
it('turns off the program picker on deferred start when the frontend selected manual wash', function (): void {
|
||||
$lane = new SelfserveProgramPickerSelectionHarness();
|
||||
$lane->setSelectedServices([]);
|
||||
|
||||
$lane->runDeferredStartRelaySideEffects();
|
||||
$lane->runDeferredStartRelaySideEffects('Manual');
|
||||
|
||||
expect($lane->programPickerWrites)->toBe([false]);
|
||||
});
|
||||
|
||||
it('does not let backend machine eligibility override a frontend manual wash selection', function (): void {
|
||||
$lane = new SelfserveProgramPickerSelectionHarness();
|
||||
$lane->setSelectedServices([]);
|
||||
$lane->setSelectedServices(['MACHINE']);
|
||||
|
||||
$lane->runDeferredStartRelaySideEffects();
|
||||
$lane->runDeferredStartRelaySideEffects('Manual');
|
||||
|
||||
expect($lane->licensePlateReads)->toBe(0)
|
||||
->and($lane->customerNumberReads)->toBe(0)
|
||||
->and($lane->programPickerWrites)->toBe([false]);
|
||||
->and($lane->programPickerWrites)->toBe([false])
|
||||
->and($lane->resolveStartWashMode('Manual'))->toBe('manual');
|
||||
});
|
||||
|
||||
it('does not infer program picker enablement from machine service without a customer machine selection', function (): void {
|
||||
$lane = new SelfserveProgramPickerSelectionHarness();
|
||||
$lane->setSelectedServices(['MACHINE']);
|
||||
|
||||
$lane->runDeferredStartRelaySideEffects();
|
||||
$lane->runNormalStartProgramPickerRelay();
|
||||
|
||||
expect($lane->programPickerWrites)->toBe([false, false])
|
||||
->and($lane->availabilityChecks)->toBe(0);
|
||||
});
|
||||
|
||||
it('keeps normal start program picker off when the customer selected manual wash', function (): void {
|
||||
$lane = new SelfserveProgramPickerSelectionHarness();
|
||||
$lane->setSelectedServices(['MACHINE']);
|
||||
|
||||
$lane->runNormalStartProgramPickerRelay('Manual');
|
||||
|
||||
expect($lane->programPickerWrites)->toBe([false])
|
||||
->and($lane->availabilityChecks)->toBe(0);
|
||||
});
|
||||
|
||||
it('honors a frontend machine wash selection when machine service is selected', function (): void {
|
||||
$lane = new SelfserveProgramPickerSelectionHarness();
|
||||
$lane->setSelectedServices(['MACHINE']);
|
||||
|
||||
$lane->runDeferredStartRelaySideEffects('Machine');
|
||||
$lane->runNormalStartProgramPickerRelay('Machine');
|
||||
|
||||
expect($lane->programPickerWrites)->toBe([true, true])
|
||||
->and($lane->availabilityChecks)->toBe(1)
|
||||
->and($lane->resolveStartWashMode('Machine'))->toBe('machine');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user