Add unit tests for STOP command flow, including relay shutoff sequencing, vehicle-type product addition, and invoice handling. Refactor STOP logic to enable relay configuration checks, error handling, and session finalization without blocking.
This commit is contained in:
@@ -46,6 +46,90 @@ trait selfserve_lane_command_t
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve whether the program selector relay currently reports ON.
|
||||
* Fail-closed to false when relay status cannot be read.
|
||||
*/
|
||||
protected function isProgramSelectorRelayOnForStop(): bool
|
||||
{
|
||||
try {
|
||||
$status = $this->getMachineProgramPickerRelayStatus();
|
||||
return (bool)($status['on'] ?? false);
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the lane vehicle-type product to the current invoice order when requested.
|
||||
*/
|
||||
protected function addVehicleTypeProductToInvoiceIfNeeded(bool $should_add): void
|
||||
{
|
||||
if (!$should_add || !method_exists($this, 'addVehicleTypeProductToLastInvoiceOrder')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->addVehicleTypeProductToLastInvoiceOrder();
|
||||
} catch (\Throwable) {
|
||||
// Never block STOP on optional order-line enrichment.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable relays after STOP in deterministic order:
|
||||
* cleaner -> machine -> program selector.
|
||||
*/
|
||||
protected function turnOffRelaysAfterStop(): void
|
||||
{
|
||||
foreach ([
|
||||
selfserve_lane_relay::MACHINE_CLEANER,
|
||||
selfserve_lane_relay::MACHINE,
|
||||
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
|
||||
] as $relay) {
|
||||
if (!$this->isRelayConfiguredForStop($relay)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$this->turnOffRelay($relay);
|
||||
} catch (\Throwable) {
|
||||
// Best effort relay shutdown; never block STOP.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function isRelayConfiguredForStop(selfserve_lane_relay $relay): bool
|
||||
{
|
||||
if (empty($this->department_lane)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$relay_id = match ($relay) {
|
||||
selfserve_lane_relay::MACHINE => (string)$this->department_lane->relay_machine_id->value(),
|
||||
selfserve_lane_relay::MACHINE_PROGRAM_PICKER => (string)$this->department_lane->relay_machine_program_picker_id->value(),
|
||||
selfserve_lane_relay::MACHINE_CLEANER => (string)$this->department_lane->relay_machine_cleaner_id->value(),
|
||||
};
|
||||
|
||||
return trim($relay_id) !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize any active self-serve wash session before resetting lane state.
|
||||
*/
|
||||
protected function completeLatestSessionForStop(): void
|
||||
{
|
||||
try {
|
||||
(new \modules\selfserve\classes\selfserve_wash_flow())->completeLatestSessionForLane(
|
||||
$this->id,
|
||||
$this->getLicensePlate() ?: null,
|
||||
$this->getCustomerNumber() ?: null,
|
||||
method_exists($this, 'getLastInvoiceOrderId') ? $this->getLastInvoiceOrderId() : null
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
// Session completion must not block STOP flow.
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Execute a command on a self-serve lane
|
||||
* @param selfserve_lane_command $command The command to execute
|
||||
@@ -117,34 +201,20 @@ trait selfserve_lane_command_t
|
||||
if (($this->getCustomerNumber() !== $arguments->customer_number) && !$this->isBypassCustomerNumberValidation()) {
|
||||
throw new \InvalidArgumentException("Customer number mismatch: Lane customer number " . $this->getCustomerNumber() . " does not match argument customer number " . $arguments->customer_number);
|
||||
}
|
||||
// Snapshot program selector state before relay shutdown.
|
||||
$program_selector_on = $this->isProgramSelectorRelayOnForStop();
|
||||
// Invoice the customer
|
||||
$this->invoice();
|
||||
// If program selector was ON, add vehicle-type product to the self-serve order.
|
||||
$this->addVehicleTypeProductToInvoiceIfNeeded($program_selector_on);
|
||||
// Open the exit port
|
||||
$this->open(selfserve_lane_port::EXIT);
|
||||
// If department has enabled self-serve, turn off the machine relay when stopping
|
||||
try {
|
||||
if ($this->isDepartmentSelfServeEnabled()) {
|
||||
// Only attempt if a machine relay is configured for this lane
|
||||
if (!empty($this->department_lane) && !empty($this->department_lane->relay_machine_id) && !empty($this->department_lane->relay_machine_id->value())) {
|
||||
$this->turnOffRelay(selfserve_lane_relay::MACHINE);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Swallow relay control errors to not block STOP flow
|
||||
}
|
||||
// Turn off configured relays in deterministic order.
|
||||
$this->turnOffRelaysAfterStop();
|
||||
// Log the lane stop event
|
||||
$this->logLaneAction(selfserve_lane_log_action::STOP_WASH);
|
||||
// Finalize any active self-serve wash session before resetting lane state.
|
||||
try {
|
||||
(new \modules\selfserve\classes\selfserve_wash_flow())->completeLatestSessionForLane(
|
||||
$this->id,
|
||||
$this->getLicensePlate() ?: null,
|
||||
$this->getCustomerNumber() ?: null,
|
||||
method_exists($this, 'getLastInvoiceOrderId') ? $this->getLastInvoiceOrderId() : null
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
// Session completion must not block STOP flow.
|
||||
}
|
||||
$this->completeLatestSessionForStop();
|
||||
// Reset the lane
|
||||
self::execute(selfserve_lane_command::RESET, new selfserve_lane_command_arguments());
|
||||
break;
|
||||
|
||||
@@ -8,11 +8,13 @@ use classes\selfserve;
|
||||
use Exception;
|
||||
use modules\selfserve\classes\selfserve_lane;
|
||||
use modules\selfserve\helpers\selfserve_lane_status;
|
||||
use objects\customer_vehicles_o;
|
||||
use objects\order_items_o;
|
||||
use objects\orders_o;
|
||||
|
||||
trait selfserve_lane_invoice_t
|
||||
{
|
||||
private const INVOICE_SYSTEM_USER_ID = 2285;
|
||||
public ?int $last_invoice_order_id = null;
|
||||
|
||||
/**
|
||||
@@ -61,7 +63,7 @@ trait selfserve_lane_invoice_t
|
||||
// Create invoice order
|
||||
$order = (new orders_o())->add(
|
||||
$this->getCustomerNumber(),
|
||||
2285, // System User ID
|
||||
self::INVOICE_SYSTEM_USER_ID,
|
||||
'',
|
||||
'',
|
||||
(int)$this->department_lane->department->value(),
|
||||
@@ -74,9 +76,50 @@ trait selfserve_lane_invoice_t
|
||||
$order_items->addItemToOrder(
|
||||
$order->id,
|
||||
$product_id,
|
||||
2285, // System User ID
|
||||
self::INVOICE_SYSTEM_USER_ID,
|
||||
$amount,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the current vehicle type product as a single order line on the latest invoice order.
|
||||
* Returns true when the line was added, false when vehicle/product context was unavailable.
|
||||
*/
|
||||
public function addVehicleTypeProductToLastInvoiceOrder(): bool
|
||||
{
|
||||
if (empty($this->last_invoice_order_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$vehicle_type_product_id = $this->resolveVehicleTypeProductIdForInvoice();
|
||||
if ($vehicle_type_product_id === null || $vehicle_type_product_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
(new order_items_o())->addItemToOrder(
|
||||
(int)$this->last_invoice_order_id,
|
||||
$vehicle_type_product_id,
|
||||
self::INVOICE_SYSTEM_USER_ID,
|
||||
1
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function resolveVehicleTypeProductIdForInvoice(): ?int
|
||||
{
|
||||
$license_plate = trim((string)$this->getLicensePlate());
|
||||
if ($license_plate === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$vehicle = (new customer_vehicles_o())->selectByPlate(selfserve::standardize_registration($license_plate));
|
||||
if (!$vehicle->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$product_id = (int)$vehicle->type->value();
|
||||
return $product_id > 0 ? $product_id : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
app_require('modules/selfserve/traits/selfserve_lane_command_t.php');
|
||||
app_require('modules/selfserve/helpers/selfserve_lane_command.php');
|
||||
app_require('modules/selfserve/helpers/selfserve_lane_mode.php');
|
||||
app_require('modules/selfserve/helpers/selfserve_lane_port.php');
|
||||
app_require('modules/selfserve/helpers/selfserve_lane_relay.php');
|
||||
app_require('modules/selfserve/helpers/selfserve_lane_state.php');
|
||||
app_require('modules/selfserve/helpers/selfserve_lane_status.php');
|
||||
app_require('modules/selfserve/classes/selfserve_lane_command_arguments.php');
|
||||
|
||||
use modules\selfserve\classes\selfserve_lane_command_arguments;
|
||||
use modules\selfserve\helpers\selfserve_lane_command;
|
||||
use modules\selfserve\helpers\selfserve_lane_mode;
|
||||
use modules\selfserve\helpers\selfserve_lane_port;
|
||||
use modules\selfserve\helpers\selfserve_lane_relay;
|
||||
use modules\selfserve\helpers\selfserve_lane_state;
|
||||
use modules\selfserve\helpers\selfserve_lane_status;
|
||||
use modules\selfserve\traits\selfserve_lane_command_t;
|
||||
|
||||
class SelfserveLaneCommandValueFake
|
||||
{
|
||||
public function __construct(private readonly string $value) {}
|
||||
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
|
||||
class SelfserveLaneCommandDepartmentLaneFake
|
||||
{
|
||||
public SelfserveLaneCommandValueFake $relay_machine_id;
|
||||
public SelfserveLaneCommandValueFake $relay_machine_program_picker_id;
|
||||
public SelfserveLaneCommandValueFake $relay_machine_cleaner_id;
|
||||
|
||||
public function __construct(string $machineRelayId, string $programRelayId, string $cleanerRelayId)
|
||||
{
|
||||
$this->relay_machine_id = new SelfserveLaneCommandValueFake($machineRelayId);
|
||||
$this->relay_machine_program_picker_id = new SelfserveLaneCommandValueFake($programRelayId);
|
||||
$this->relay_machine_cleaner_id = new SelfserveLaneCommandValueFake($cleanerRelayId);
|
||||
}
|
||||
}
|
||||
|
||||
class SelfserveLaneStopFlowHarness
|
||||
{
|
||||
use selfserve_lane_command_t;
|
||||
|
||||
public const DEFAULT_WASH_START_TIME = 0;
|
||||
public const DEFAULT_CUSTOMER_NUMBER = 0;
|
||||
public const DEFAULT_LICENSE_PLATE = '';
|
||||
|
||||
public int $id = 77;
|
||||
public object $department_lane;
|
||||
public int $invoiceCalls = 0;
|
||||
public int $vehicleTypeProductAddCalls = 0;
|
||||
/** @var selfserve_lane_port[] */
|
||||
public array $openedPorts = [];
|
||||
/** @var selfserve_lane_relay[] */
|
||||
public array $turnedOffRelays = [];
|
||||
|
||||
private selfserve_lane_status $laneStatus;
|
||||
private selfserve_lane_mode $laneMode;
|
||||
private selfserve_lane_state $laneState;
|
||||
private int $customerNumber = 1234;
|
||||
private string $licensePlate = 'AB12345';
|
||||
private int $washStartTime = 120;
|
||||
private ?int $reservationStartTime = null;
|
||||
private bool $bypassCustomerValidation = false;
|
||||
private bool $programSelectorOn;
|
||||
|
||||
public function __construct(
|
||||
bool $programSelectorOn,
|
||||
string $machineRelayId = 'relay-machine',
|
||||
string $programRelayId = 'relay-program',
|
||||
string $cleanerRelayId = 'relay-cleaner'
|
||||
) {
|
||||
$this->programSelectorOn = $programSelectorOn;
|
||||
$this->department_lane = new SelfserveLaneCommandDepartmentLaneFake(
|
||||
$machineRelayId,
|
||||
$programRelayId,
|
||||
$cleanerRelayId
|
||||
);
|
||||
$this->laneStatus = selfserve_lane_status::OCCUPIED;
|
||||
$this->laneMode = selfserve_lane_mode::MANUAL;
|
||||
$this->laneState = selfserve_lane_state::IN_WASH;
|
||||
}
|
||||
|
||||
public function getLaneStatus(): selfserve_lane_status
|
||||
{
|
||||
return $this->laneStatus;
|
||||
}
|
||||
|
||||
public function setLaneStatus(selfserve_lane_status $status): void
|
||||
{
|
||||
$this->laneStatus = $status;
|
||||
}
|
||||
|
||||
public function setLaneMode(selfserve_lane_mode $mode): void
|
||||
{
|
||||
$this->laneMode = $mode;
|
||||
}
|
||||
|
||||
public function setLaneState(selfserve_lane_state $state): void
|
||||
{
|
||||
$this->laneState = $state;
|
||||
}
|
||||
|
||||
public function setWashStartTime(?int $time): void
|
||||
{
|
||||
$this->washStartTime = $time ?? self::DEFAULT_WASH_START_TIME;
|
||||
}
|
||||
|
||||
public function setCustomerNumber(?int $customerNumber): void
|
||||
{
|
||||
$this->customerNumber = $customerNumber ?? self::DEFAULT_CUSTOMER_NUMBER;
|
||||
}
|
||||
|
||||
public function getCustomerNumber(): int
|
||||
{
|
||||
return $this->customerNumber;
|
||||
}
|
||||
|
||||
public function setLicensePlate(?string $licensePlate): void
|
||||
{
|
||||
$this->licensePlate = $licensePlate ?? self::DEFAULT_LICENSE_PLATE;
|
||||
}
|
||||
|
||||
public function getLicensePlate(): string
|
||||
{
|
||||
return $this->licensePlate;
|
||||
}
|
||||
|
||||
public function setReservationStartTime(?int $reservationStartTime): void
|
||||
{
|
||||
$this->reservationStartTime = $reservationStartTime;
|
||||
}
|
||||
|
||||
public function isBypassCustomerNumberValidation(): bool
|
||||
{
|
||||
return $this->bypassCustomerValidation;
|
||||
}
|
||||
|
||||
public function invoice(): bool
|
||||
{
|
||||
$this->invoiceCalls++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function addVehicleTypeProductToLastInvoiceOrder(): bool
|
||||
{
|
||||
$this->vehicleTypeProductAddCalls++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getMachineProgramPickerRelayStatus(): array
|
||||
{
|
||||
return ['on' => $this->programSelectorOn];
|
||||
}
|
||||
|
||||
public function open(selfserve_lane_port $port): bool
|
||||
{
|
||||
$this->openedPorts[] = $port;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function turnOffRelay(selfserve_lane_relay $relay): bool
|
||||
{
|
||||
$this->turnedOffRelays[] = $relay;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function logLaneAction(...$args): void {}
|
||||
|
||||
protected function completeLatestSessionForStop(): void
|
||||
{
|
||||
// No-op in unit tests.
|
||||
}
|
||||
}
|
||||
|
||||
it('adds vehicle type product on STOP when program selector is on, then turns off cleaner machine and selector relays', function (): void {
|
||||
$lane = new SelfserveLaneStopFlowHarness(programSelectorOn: true);
|
||||
$args = (new selfserve_lane_command_arguments())->setCustomerNumber(1234);
|
||||
|
||||
$lane->execute(selfserve_lane_command::STOP, $args);
|
||||
|
||||
expect($lane->invoiceCalls)->toBe(1);
|
||||
expect($lane->vehicleTypeProductAddCalls)->toBe(1);
|
||||
expect($lane->openedPorts)->toBe([selfserve_lane_port::EXIT]);
|
||||
expect($lane->turnedOffRelays)->toBe([
|
||||
selfserve_lane_relay::MACHINE_CLEANER,
|
||||
selfserve_lane_relay::MACHINE,
|
||||
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
|
||||
]);
|
||||
expect($lane->getLaneStatus())->toBe(selfserve_lane_status::AVAILABLE);
|
||||
});
|
||||
|
||||
it('skips vehicle type product add when program selector is off and only disables configured relays', function (): void {
|
||||
$lane = new SelfserveLaneStopFlowHarness(
|
||||
programSelectorOn: false,
|
||||
machineRelayId: 'relay-machine',
|
||||
programRelayId: 'relay-program',
|
||||
cleanerRelayId: ''
|
||||
);
|
||||
$args = (new selfserve_lane_command_arguments())->setCustomerNumber(1234);
|
||||
|
||||
$lane->execute(selfserve_lane_command::STOP, $args);
|
||||
|
||||
expect($lane->invoiceCalls)->toBe(1);
|
||||
expect($lane->vehicleTypeProductAddCalls)->toBe(0);
|
||||
expect($lane->turnedOffRelays)->toBe([
|
||||
selfserve_lane_relay::MACHINE,
|
||||
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user