Auto-disable department self-serve at opening (#323)
Auto-disable department self-serve at opening
This commit is contained in:
@@ -4157,6 +4157,19 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
auto_deactivation:
|
||||||
|
type: object
|
||||||
|
required: [at, timezone, label]
|
||||||
|
properties:
|
||||||
|
at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
timezone:
|
||||||
|
type: string
|
||||||
|
example: Europe/Copenhagen
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
'404':
|
'404':
|
||||||
$ref: '#/components/responses/NotFound'
|
$ref: '#/components/responses/NotFound'
|
||||||
put:
|
put:
|
||||||
@@ -4189,6 +4202,21 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
auto_deactivation:
|
||||||
|
type: object
|
||||||
|
required: [at, timezone, label]
|
||||||
|
properties:
|
||||||
|
at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
timezone:
|
||||||
|
type: string
|
||||||
|
example: Europe/Copenhagen
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
'404':
|
'404':
|
||||||
$ref: '#/components/responses/NotFound'
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ use goals\helpers\goals_criteria_progress_alert_frequency as Freq;
|
|||||||
use objects\department_lanes_o;
|
use objects\department_lanes_o;
|
||||||
use objects\department_goals_o;
|
use objects\department_goals_o;
|
||||||
use objects\department_selfserve_tasks_o;
|
use objects\department_selfserve_tasks_o;
|
||||||
|
use objects\department_variables_o;
|
||||||
use objects\departments_o;
|
use objects\departments_o;
|
||||||
use objects\bookings_o;
|
use objects\bookings_o;
|
||||||
use objects\logs_o;
|
use objects\logs_o;
|
||||||
@@ -1524,6 +1525,7 @@ function SelfserveOpeningRelayActivationCron(): array
|
|||||||
$candidates = selfserveOpeningCleanerRelayActivationCandidates($now);
|
$candidates = selfserveOpeningCleanerRelayActivationCandidates($now);
|
||||||
$summary = [
|
$summary = [
|
||||||
'checked_departments' => count($candidates),
|
'checked_departments' => count($candidates),
|
||||||
|
'disabled_departments' => 0,
|
||||||
'activated_departments' => 0,
|
'activated_departments' => 0,
|
||||||
'skipped_departments' => 0,
|
'skipped_departments' => 0,
|
||||||
'failed_departments' => 0,
|
'failed_departments' => 0,
|
||||||
@@ -1535,18 +1537,45 @@ function SelfserveOpeningRelayActivationCron(): array
|
|||||||
foreach ($candidates as $candidate) {
|
foreach ($candidates as $candidate) {
|
||||||
$departmentId = (int)($candidate['department_id'] ?? 0);
|
$departmentId = (int)($candidate['department_id'] ?? 0);
|
||||||
$opensAt = (string)($candidate['opens_at'] ?? '');
|
$opensAt = (string)($candidate['opens_at'] ?? '');
|
||||||
|
$openingDate = (string)($candidate['opening_date'] ?? $now->format('Y-m-d'));
|
||||||
if ($departmentId <= 0 || $opensAt === '') {
|
if ($departmentId <= 0 || $opensAt === '') {
|
||||||
$summary['skipped_departments']++;
|
$summary['skipped_departments']++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cacheKey = selfserveOpeningCleanerRelayActivationKey($departmentId, $opensAt, $now);
|
$cacheKey = selfserveOpeningCleanerRelayActivationKey($departmentId, $opensAt, $openingDate);
|
||||||
if (selfserveOpeningCleanerRelayActivationAlreadyHandled($cacheKey)) {
|
|
||||||
$summary['skipped_departments']++;
|
try {
|
||||||
|
$transition = department_variables_o::withSelfServeTransitionLock(
|
||||||
|
$departmentId,
|
||||||
|
static function () use ($departmentId): array {
|
||||||
|
$departmentSummary = selfserveActivateStaffedDefaultRelaysForDepartment($departmentId);
|
||||||
|
if ((int)$departmentSummary['failed'] > 0) {
|
||||||
|
return [
|
||||||
|
'department_summary' => $departmentSummary,
|
||||||
|
'department_disabled' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'department_summary' => $departmentSummary,
|
||||||
|
'department_disabled' => selfserveDisableDepartmentSelfServeForOpening($departmentId),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
$summary['failed_departments']++;
|
||||||
|
warn(
|
||||||
|
'SelfserveOpeningRelayActivationCron could not serialize transition for department '
|
||||||
|
. $departmentId
|
||||||
|
. ': '
|
||||||
|
. $throwable->getMessage()
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$departmentSummary = selfserveActivateOpeningCleanerRelaysForDepartment($departmentId);
|
$departmentSummary = $transition['department_summary'];
|
||||||
$summary['activated_relays'] += (int)$departmentSummary['activated'];
|
$summary['activated_relays'] += (int)$departmentSummary['activated'];
|
||||||
$summary['skipped_relays'] += (int)$departmentSummary['skipped'];
|
$summary['skipped_relays'] += (int)$departmentSummary['skipped'];
|
||||||
$summary['failed_relays'] += (int)$departmentSummary['failed'];
|
$summary['failed_relays'] += (int)$departmentSummary['failed'];
|
||||||
@@ -1556,6 +1585,13 @@ function SelfserveOpeningRelayActivationCron(): array
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$departmentDisabled = (bool)$transition['department_disabled'];
|
||||||
|
if (!$departmentDisabled) {
|
||||||
|
$summary['failed_departments']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary['disabled_departments']++;
|
||||||
selfserveMarkOpeningCleanerRelayActivationHandled($cacheKey);
|
selfserveMarkOpeningCleanerRelayActivationHandled($cacheKey);
|
||||||
if ((int)$departmentSummary['activated'] > 0) {
|
if ((int)$departmentSummary['activated'] > 0) {
|
||||||
$summary['activated_departments']++;
|
$summary['activated_departments']++;
|
||||||
@@ -1565,7 +1601,8 @@ function SelfserveOpeningRelayActivationCron(): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
echo "[" . date('Y-m-d H:i:s') . "][CRON] SelfserveOpeningRelayActivationCron: "
|
echo "[" . date('Y-m-d H:i:s') . "][CRON] SelfserveOpeningRelayActivationCron: "
|
||||||
. $summary['activated_relays'] . " cleaner relays activated across "
|
. $summary['disabled_departments'] . " departments disabled self-serve, "
|
||||||
|
. $summary['activated_relays'] . " staffed-default relays activated across "
|
||||||
. $summary['activated_departments'] . " departments.\n";
|
. $summary['activated_departments'] . " departments.\n";
|
||||||
|
|
||||||
return $summary;
|
return $summary;
|
||||||
@@ -1583,23 +1620,32 @@ function selfserveOpeningCleanerRelayActivationCandidates(DateTimeImmutable $now
|
|||||||
|
|
||||||
$startColumn = $weekday . '_start';
|
$startColumn = $weekday . '_start';
|
||||||
$endColumn = $weekday . '_end';
|
$endColumn = $weekday . '_end';
|
||||||
|
$previousDate = $now->modify('-1 day');
|
||||||
|
$previousWeekday = strtolower($previousDate->format('l'));
|
||||||
|
$previousStartColumn = $previousWeekday . '_start';
|
||||||
|
$previousEndColumn = $previousWeekday . '_end';
|
||||||
$sql = "
|
$sql = "
|
||||||
SELECT
|
SELECT
|
||||||
oh.department AS department_id,
|
oh.department AS department_id,
|
||||||
TIME_FORMAT(oh.`$startColumn`, '%H:%i:%s') AS opens_at,
|
TIME_FORMAT(oh.`$startColumn`, '%H:%i:%s') AS current_opens_at,
|
||||||
TIME_FORMAT(oh.`$endColumn`, '%H:%i:%s') AS closes_at
|
TIME_FORMAT(oh.`$endColumn`, '%H:%i:%s') AS current_closes_at,
|
||||||
|
TIME_FORMAT(oh.`$previousStartColumn`, '%H:%i:%s') AS previous_opens_at,
|
||||||
|
TIME_FORMAT(oh.`$previousEndColumn`, '%H:%i:%s') AS previous_closes_at
|
||||||
FROM department_time_bookings_opening_hours oh
|
FROM department_time_bookings_opening_hours oh
|
||||||
INNER JOIN department_variables dv ON dv.department_id = oh.department
|
INNER JOIN department_variables dv ON dv.department_id = oh.department
|
||||||
INNER JOIN department_lanes dl ON dl.department = oh.department
|
|
||||||
WHERE dv.variable = 'selfserve_enabled'
|
WHERE dv.variable = 'selfserve_enabled'
|
||||||
AND LOWER(TRIM(COALESCE(dv.value, ''))) IN ('true', '1', 'yes', 'on')
|
AND LOWER(TRIM(COALESCE(dv.value, ''))) IN ('true', '1', 'yes', 'on')
|
||||||
AND oh.`$startColumn` IS NOT NULL
|
AND (
|
||||||
AND oh.`$endColumn` IS NOT NULL
|
(oh.`$startColumn` IS NOT NULL AND oh.`$endColumn` IS NOT NULL)
|
||||||
AND dl.deleted_at IS NULL
|
OR
|
||||||
AND COALESCE(dl.selfserve_enabled, 1) = 1
|
(oh.`$previousStartColumn` IS NOT NULL AND oh.`$previousEndColumn` IS NOT NULL)
|
||||||
AND dl.relay_machine_cleaner_id IS NOT NULL
|
)
|
||||||
AND TRIM(dl.relay_machine_cleaner_id) <> ''
|
GROUP BY
|
||||||
GROUP BY oh.department, oh.`$startColumn`, oh.`$endColumn`
|
oh.department,
|
||||||
|
oh.`$startColumn`,
|
||||||
|
oh.`$endColumn`,
|
||||||
|
oh.`$previousStartColumn`,
|
||||||
|
oh.`$previousEndColumn`
|
||||||
";
|
";
|
||||||
|
|
||||||
$result = $db->query($sql);
|
$result = $db->query($sql);
|
||||||
@@ -1607,14 +1653,33 @@ function selfserveOpeningCleanerRelayActivationCandidates(DateTimeImmutable $now
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return array_values(array_filter(
|
$candidates = [];
|
||||||
$db->fetch_all($result),
|
foreach ($db->fetch_all($result) as $row) {
|
||||||
static fn(array $row): bool => selfserveOpeningCleanerRelayWindowActive(
|
$currentOpensAt = (string)($row['current_opens_at'] ?? '');
|
||||||
$now,
|
$currentClosesAt = (string)($row['current_closes_at'] ?? '');
|
||||||
(string)($row['opens_at'] ?? ''),
|
if (selfserveOpeningCleanerRelayWindowActive($now, $currentOpensAt, $currentClosesAt)) {
|
||||||
(string)($row['closes_at'] ?? '')
|
$candidates[] = [
|
||||||
)
|
'department_id' => $row['department_id'] ?? null,
|
||||||
));
|
'opens_at' => $currentOpensAt,
|
||||||
|
'closes_at' => $currentClosesAt,
|
||||||
|
'opening_date' => $now->format('Y-m-d'),
|
||||||
|
];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$previousOpensAt = (string)($row['previous_opens_at'] ?? '');
|
||||||
|
$previousClosesAt = (string)($row['previous_closes_at'] ?? '');
|
||||||
|
if (selfserveOpeningCleanerRelayOvernightCarryoverActive($now, $previousOpensAt, $previousClosesAt)) {
|
||||||
|
$candidates[] = [
|
||||||
|
'department_id' => $row['department_id'] ?? null,
|
||||||
|
'opens_at' => $previousOpensAt,
|
||||||
|
'closes_at' => $previousClosesAt,
|
||||||
|
'opening_date' => $previousDate->format('Y-m-d'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selfserveOpeningCleanerRelayWindowActive(
|
function selfserveOpeningCleanerRelayWindowActive(
|
||||||
@@ -1639,6 +1704,24 @@ function selfserveOpeningCleanerRelayWindowActive(
|
|||||||
return $nowSeconds >= $opensAtSeconds;
|
return $nowSeconds >= $opensAtSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selfserveOpeningCleanerRelayOvernightCarryoverActive(
|
||||||
|
DateTimeImmutable $now,
|
||||||
|
?string $opensAt,
|
||||||
|
?string $closesAt
|
||||||
|
): bool {
|
||||||
|
$opensAtSeconds = selfserveOpeningCleanerRelayTimeToSeconds($opensAt);
|
||||||
|
$closesAtSeconds = selfserveOpeningCleanerRelayTimeToSeconds($closesAt);
|
||||||
|
if ($opensAtSeconds === null || $closesAtSeconds === null || $opensAtSeconds <= $closesAtSeconds) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nowSeconds = ((int)$now->format('G') * 3600)
|
||||||
|
+ ((int)$now->format('i') * 60)
|
||||||
|
+ (int)$now->format('s');
|
||||||
|
|
||||||
|
return $nowSeconds < $closesAtSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
function selfserveOpeningCleanerRelayTimeToSeconds(?string $time): ?int
|
function selfserveOpeningCleanerRelayTimeToSeconds(?string $time): ?int
|
||||||
{
|
{
|
||||||
$time = trim((string)$time);
|
$time = trim((string)$time);
|
||||||
@@ -1661,7 +1744,27 @@ function selfserveOpeningCleanerRelayTimeToSeconds(?string $time): ?int
|
|||||||
return ($hours * 3600) + ($minutes * 60) + $seconds;
|
return ($hours * 3600) + ($minutes * 60) + $seconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selfserveActivateOpeningCleanerRelaysForDepartment(int $departmentId): array
|
function selfserveDisableDepartmentSelfServeForOpening(int $departmentId): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(new department_variables_o())
|
||||||
|
->selectDepartment($departmentId)
|
||||||
|
->set('selfserve_enabled', 'false');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
warn(
|
||||||
|
'SelfserveOpeningRelayActivationCron failed to disable self-serve for department '
|
||||||
|
. $departmentId
|
||||||
|
. ': '
|
||||||
|
. $throwable->getMessage()
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selfserveActivateStaffedDefaultRelaysForDepartment(int $departmentId): array
|
||||||
{
|
{
|
||||||
$summary = [
|
$summary = [
|
||||||
'activated' => 0,
|
'activated' => 0,
|
||||||
@@ -1677,17 +1780,7 @@ function selfserveActivateOpeningCleanerRelaysForDepartment(int $departmentId):
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!selfserveLaneHasAnyConfiguredStaffedDefaultRelay($departmentLane)) {
|
||||||
if (!$departmentLane->isSelfServeEnabled()) {
|
|
||||||
$summary['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} catch (Throwable) {
|
|
||||||
$summary['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selfserveLaneHasConfiguredCleanerRelay($departmentLane)) {
|
|
||||||
$summary['skipped']++;
|
$summary['skipped']++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1700,13 +1793,15 @@ function selfserveActivateOpeningCleanerRelaysForDepartment(int $departmentId):
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$lane = $selfserve->lane($laneId);
|
$lane = $selfserve->lane($laneId);
|
||||||
if (!selfserveLaneHasConfiguredCleanerRelay($lane->department_lane)) {
|
$summary['activated'] += selfserveSetOptionalLaneRelayState($lane, 'relay_machine_program_picker_id', static function () use ($lane): void {
|
||||||
$summary['skipped']++;
|
$lane->setMachineProgramPickerRelayStatusForDepartmentOperation(true);
|
||||||
continue;
|
});
|
||||||
}
|
$summary['activated'] += selfserveSetOptionalLaneRelayState($lane, 'relay_machine_cleaner_id', static function () use ($lane): void {
|
||||||
|
$lane->setMachineCleanerRelayStatusForDepartmentOperation(true);
|
||||||
$lane->setMachineCleanerRelayStatusHard(true);
|
});
|
||||||
$summary['activated']++;
|
$summary['activated'] += selfserveSetOptionalLaneRelayState($lane, 'relay_machine_id', static function () use ($lane): void {
|
||||||
|
$lane->setMachineRelayStatusForDepartmentOperation(true);
|
||||||
|
});
|
||||||
} catch (Throwable $throwable) {
|
} catch (Throwable $throwable) {
|
||||||
$summary['failed']++;
|
$summary['failed']++;
|
||||||
warn(
|
warn(
|
||||||
@@ -1723,19 +1818,31 @@ function selfserveActivateOpeningCleanerRelaysForDepartment(int $departmentId):
|
|||||||
return $summary;
|
return $summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selfserveLaneHasAnyConfiguredStaffedDefaultRelay(?object $departmentLane): bool
|
||||||
|
{
|
||||||
|
return selfserveLaneHasConfiguredRelay($departmentLane, 'relay_machine_program_picker_id')
|
||||||
|
|| selfserveLaneHasConfiguredRelay($departmentLane, 'relay_machine_cleaner_id')
|
||||||
|
|| selfserveLaneHasConfiguredRelay($departmentLane, 'relay_machine_id');
|
||||||
|
}
|
||||||
|
|
||||||
function selfserveLaneHasConfiguredCleanerRelay(?object $departmentLane): bool
|
function selfserveLaneHasConfiguredCleanerRelay(?object $departmentLane): bool
|
||||||
|
{
|
||||||
|
return selfserveLaneHasConfiguredRelay($departmentLane, 'relay_machine_cleaner_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
function selfserveLaneHasConfiguredRelay(?object $departmentLane, string $relayProperty): bool
|
||||||
{
|
{
|
||||||
if (
|
if (
|
||||||
$departmentLane === null
|
$departmentLane === null
|
||||||
|| !isset($departmentLane->relay_machine_cleaner_id)
|
|| !isset($departmentLane->{$relayProperty})
|
||||||
|| !is_object($departmentLane->relay_machine_cleaner_id)
|
|| !is_object($departmentLane->{$relayProperty})
|
||||||
|| !method_exists($departmentLane->relay_machine_cleaner_id, 'value')
|
|| !method_exists($departmentLane->{$relayProperty}, 'value')
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$relayId = trim((string)$departmentLane->relay_machine_cleaner_id->value());
|
$relayId = trim((string)$departmentLane->{$relayProperty}->value());
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1743,14 +1850,35 @@ function selfserveLaneHasConfiguredCleanerRelay(?object $departmentLane): bool
|
|||||||
return $relayId !== '' && $relayId !== '0' && strtolower($relayId) !== 'null';
|
return $relayId !== '' && $relayId !== '0' && strtolower($relayId) !== 'null';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selfserveSetOptionalLaneRelayState(object $lane, string $relayProperty, callable $callback): int
|
||||||
|
{
|
||||||
|
if (!selfserveLaneHasConfiguredRelay($lane->department_lane ?? null, $relayProperty)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$callback();
|
||||||
|
return 1;
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
warn(
|
||||||
|
'SelfserveOpeningRelayActivationCron failed to activate relay '
|
||||||
|
. $relayProperty
|
||||||
|
. ': '
|
||||||
|
. $throwable->getMessage()
|
||||||
|
);
|
||||||
|
|
||||||
|
throw $throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selfserveOpeningCleanerRelayActivationKey(
|
function selfserveOpeningCleanerRelayActivationKey(
|
||||||
int $departmentId,
|
int $departmentId,
|
||||||
string $opensAt,
|
string $opensAt,
|
||||||
DateTimeImmutable $now
|
string $openingDate
|
||||||
): string {
|
): string {
|
||||||
$normalizedOpensAt = preg_replace('/[^0-9]/', '', $opensAt) ?: 'unknown';
|
$normalizedOpensAt = preg_replace('/[^0-9]/', '', $opensAt) ?: 'unknown';
|
||||||
return 'selfserve:opening-cleaner-relays:'
|
return 'selfserve:opening-cleaner-relays:'
|
||||||
. $now->format('Y-m-d')
|
. $openingDate
|
||||||
. ':'
|
. ':'
|
||||||
. $departmentId
|
. $departmentId
|
||||||
. ':'
|
. ':'
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ return [
|
|||||||
[
|
[
|
||||||
'id' => 'selfserve.activate_opening_cleaner_relays',
|
'id' => 'selfserve.activate_opening_cleaner_relays',
|
||||||
'legacy_name' => 'SelfserveOpeningRelayActivationCron',
|
'legacy_name' => 'SelfserveOpeningRelayActivationCron',
|
||||||
'name' => 'Activate self-serve opening cleaner relays',
|
'name' => 'Restore staffed relays and disable department self-serve at opening',
|
||||||
'description' => 'Turns configured self-serve cleaner relays on when a department enters opening hours.',
|
'description' => 'Restores every configured lane relay to its staffed default before disabling department self-serve when staffed opening hours begin.',
|
||||||
'module' => 'selfserve',
|
'module' => 'selfserve',
|
||||||
'handler' => 'SelfserveOpeningRelayActivationCron',
|
'handler' => 'SelfserveOpeningRelayActivationCron',
|
||||||
'schedule' => ['type' => 'interval', 'seconds' => 60],
|
'schedule' => ['type' => 'interval', 'seconds' => 60],
|
||||||
|
|||||||
@@ -130,6 +130,11 @@ trait selfserve_lane_relay_controller_t
|
|||||||
return $this->setRelayStatusHard(selfserve_lane_relay::MACHINE, $on);
|
return $this->setRelayStatusHard(selfserve_lane_relay::MACHINE, $on);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setMachineRelayStatusForDepartmentOperation(bool $on): bool
|
||||||
|
{
|
||||||
|
return $this->sendRelaySwitchCommandForDepartmentOperation(selfserve_lane_relay::MACHINE, $on);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set PROGRAM SELECTOR relay status directly, bypassing lane status guards.
|
* Set PROGRAM SELECTOR relay status directly, bypassing lane status guards.
|
||||||
* Intended for department-level operational toggles.
|
* Intended for department-level operational toggles.
|
||||||
@@ -161,6 +166,14 @@ trait selfserve_lane_relay_controller_t
|
|||||||
return $this->setRelayStatusHard(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $on);
|
return $this->setRelayStatusHard(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $on);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setMachineProgramPickerRelayStatusForDepartmentOperation(bool $on): bool
|
||||||
|
{
|
||||||
|
return $this->sendRelaySwitchCommandForDepartmentOperation(
|
||||||
|
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
|
||||||
|
$on
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set MACHINE_CLEANER relay status directly.
|
* Set MACHINE_CLEANER relay status directly.
|
||||||
* @param bool $on true to turn on, false to turn off
|
* @param bool $on true to turn on, false to turn off
|
||||||
@@ -182,6 +195,14 @@ trait selfserve_lane_relay_controller_t
|
|||||||
return $this->setRelayStatusHard(selfserve_lane_relay::MACHINE_CLEANER, $on);
|
return $this->setRelayStatusHard(selfserve_lane_relay::MACHINE_CLEANER, $on);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setMachineCleanerRelayStatusForDepartmentOperation(bool $on): bool
|
||||||
|
{
|
||||||
|
return $this->sendRelaySwitchCommandForDepartmentOperation(
|
||||||
|
selfserve_lane_relay::MACHINE_CLEANER,
|
||||||
|
$on
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set relay status directly for a specific relay type.
|
* Set relay status directly for a specific relay type.
|
||||||
* @param selfserve_lane_relay $relay
|
* @param selfserve_lane_relay $relay
|
||||||
@@ -1033,6 +1054,20 @@ trait selfserve_lane_relay_controller_t
|
|||||||
throw new \Exception("Cannot change relay state: Self-serve is not enabled for this lane's department.");
|
throw new \Exception("Cannot change relay state: Self-serve is not enabled for this lane's department.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this->sendRelaySwitchCommandForDepartmentOperation($relay, $on, $duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Department-level transitions deliberately bypass lane self-serve guards.
|
||||||
|
* Callers must serialize the transition before using this path.
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function sendRelaySwitchCommandForDepartmentOperation(
|
||||||
|
selfserve_lane_relay $relay,
|
||||||
|
bool $on,
|
||||||
|
?int $duration = null
|
||||||
|
): bool {
|
||||||
$relay_id = $this->getRelayId($relay);
|
$relay_id = $this->getRelayId($relay);
|
||||||
$payload = [
|
$payload = [
|
||||||
'id' => $relay_id,
|
'id' => $relay_id,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace objects;
|
|||||||
|
|
||||||
use classes\db;
|
use classes\db;
|
||||||
use classes\object_property;
|
use classes\object_property;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Exception;
|
use Exception;
|
||||||
use traits\db_object_t;
|
use traits\db_object_t;
|
||||||
|
|
||||||
@@ -81,6 +82,22 @@ class department_time_bookings_opening_hours_o extends db
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function selectExistingByDepartment(int $department_id): bool
|
||||||
|
{
|
||||||
|
$matches = self::getFieldsWhere([
|
||||||
|
'department' => $department_id
|
||||||
|
], [
|
||||||
|
'id'
|
||||||
|
]);
|
||||||
|
if (count($matches) === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->id = $matches[0]['id'];
|
||||||
|
self::getObjectProperties();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public function getObjectProperties(): void
|
public function getObjectProperties(): void
|
||||||
{
|
{
|
||||||
$this->department = new object_property($this->table, $this->id, 'department', 'int', true);
|
$this->department = new object_property($this->table, $this->id, 'department', 'int', true);
|
||||||
@@ -197,6 +214,90 @@ class department_time_bookings_opening_hours_o extends db
|
|||||||
return ($startTime >= $openingStartTime && $endTime <= $openingEndTime);
|
return ($startTime >= $openingStartTime && $endTime <= $openingEndTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getNextOpeningStart(DateTimeImmutable $now): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
self::requireSelected();
|
||||||
|
|
||||||
|
$previousDay = $now->modify('-1 day');
|
||||||
|
$previousWeekdayName = self::getWeekdayName((int)$previousDay->format('N'));
|
||||||
|
$previousOpeningStart = $this->normalizeOpeningTime(
|
||||||
|
$this->{"{$previousWeekdayName}_start"}->value()
|
||||||
|
);
|
||||||
|
$previousOpeningEnd = $this->normalizeOpeningTime(
|
||||||
|
$this->{"{$previousWeekdayName}_end"}->value()
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
$previousOpeningStart !== null
|
||||||
|
&& $previousOpeningEnd !== null
|
||||||
|
&& $previousOpeningStart > $previousOpeningEnd
|
||||||
|
) {
|
||||||
|
$carryoverEnd = $now->setTime(
|
||||||
|
$previousOpeningEnd[0],
|
||||||
|
$previousOpeningEnd[1],
|
||||||
|
$previousOpeningEnd[2]
|
||||||
|
);
|
||||||
|
if ($now < $carryoverEnd) {
|
||||||
|
return $now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($dayOffset = 0; $dayOffset <= 7; $dayOffset++) {
|
||||||
|
$candidateDay = $now->modify('+' . $dayOffset . ' days');
|
||||||
|
$weekdayName = self::getWeekdayName((int)$candidateDay->format('N'));
|
||||||
|
$openingStart = $this->normalizeOpeningTime($this->{"{$weekdayName}_start"}->value());
|
||||||
|
$openingEnd = $this->normalizeOpeningTime($this->{"{$weekdayName}_end"}->value());
|
||||||
|
|
||||||
|
if (
|
||||||
|
$openingStart === null
|
||||||
|
|| $openingEnd === null
|
||||||
|
|| $openingStart === $openingEnd
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidateStart = $candidateDay->setTime($openingStart[0], $openingStart[1], $openingStart[2]);
|
||||||
|
if ($dayOffset === 0) {
|
||||||
|
$candidateEnd = $candidateDay->setTime($openingEnd[0], $openingEnd[1], $openingEnd[2]);
|
||||||
|
if ($candidateEnd <= $candidateStart) {
|
||||||
|
$candidateEnd = $candidateEnd->modify('+1 day');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($now >= $candidateStart && $now < $candidateEnd) {
|
||||||
|
return $now;
|
||||||
|
}
|
||||||
|
if ($candidateStart < $now) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $candidateStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeOpeningTime(mixed $value): ?array
|
||||||
|
{
|
||||||
|
$value = trim((string)$value);
|
||||||
|
if ($value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/', $value, $matches)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hours = (int)$matches[1];
|
||||||
|
$minutes = (int)$matches[2];
|
||||||
|
$seconds = isset($matches[3]) ? (int)$matches[3] : 0;
|
||||||
|
|
||||||
|
if ($hours > 23 || $minutes > 59 || $seconds > 59) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$hours, $minutes, $seconds];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the name of the day of the week
|
* Get the name of the day of the week
|
||||||
* @param int $dayOfWeek The day of the week (1 = Monday, 7 = Sunday)
|
* @param int $dayOfWeek The day of the week (1 = Monday, 7 = Sunday)
|
||||||
|
|||||||
@@ -59,6 +59,68 @@ class department_variables_o extends db
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function withSelfServeTransitionLock(
|
||||||
|
int $departmentId,
|
||||||
|
callable $callback,
|
||||||
|
int $timeoutSeconds = 5
|
||||||
|
): mixed {
|
||||||
|
if ($departmentId <= 0) {
|
||||||
|
throw new \InvalidArgumentException('A valid department is required for the self-serve transition lock.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lockName = 'selfserve:department-transition:' . $departmentId;
|
||||||
|
if (!self::acquireSelfServeTransitionLock($lockName, max(0, $timeoutSeconds))) {
|
||||||
|
throw new \RuntimeException('The department self-serve transition is already in progress.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $callback();
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
self::releaseSelfServeTransitionLock($lockName);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function acquireSelfServeTransitionLock(string $lockName, int $timeoutSeconds): bool
|
||||||
|
{
|
||||||
|
global $db;
|
||||||
|
|
||||||
|
$statement = $db->prepare('SELECT GET_LOCK(?, ?) AS acquired');
|
||||||
|
if ($statement === false) {
|
||||||
|
throw new \RuntimeException('Could not prepare the department self-serve transition lock.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement->bind_param('si', $lockName, $timeoutSeconds);
|
||||||
|
$statement->execute();
|
||||||
|
$result = $statement->get_result();
|
||||||
|
$row = $result instanceof \mysqli_result ? $result->fetch_assoc() : null;
|
||||||
|
|
||||||
|
return (int)($row['acquired'] ?? 0) === 1;
|
||||||
|
} finally {
|
||||||
|
$statement->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function releaseSelfServeTransitionLock(string $lockName): void
|
||||||
|
{
|
||||||
|
global $db;
|
||||||
|
|
||||||
|
$statement = $db->prepare('SELECT RELEASE_LOCK(?) AS released');
|
||||||
|
if ($statement === false) {
|
||||||
|
throw new \RuntimeException('Could not prepare the department self-serve transition lock release.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement->bind_param('s', $lockName);
|
||||||
|
$statement->execute();
|
||||||
|
} finally {
|
||||||
|
$statement->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Require the department ID to be set, and throw an exception if it is not
|
* Require the department ID to be set, and throw an exception if it is not
|
||||||
* @throws Exception If the department ID is not set.
|
* @throws Exception If the department ID is not set.
|
||||||
|
|||||||
@@ -4600,6 +4600,19 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
auto_deactivation:
|
||||||
|
type: object
|
||||||
|
required: [at, timezone, label]
|
||||||
|
properties:
|
||||||
|
at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
timezone:
|
||||||
|
type: string
|
||||||
|
example: Europe/Copenhagen
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
'404':
|
'404':
|
||||||
$ref: '#/components/responses/NotFound'
|
$ref: '#/components/responses/NotFound'
|
||||||
put:
|
put:
|
||||||
@@ -4632,6 +4645,21 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
auto_deactivation:
|
||||||
|
type: object
|
||||||
|
required: [at, timezone, label]
|
||||||
|
properties:
|
||||||
|
at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
timezone:
|
||||||
|
type: string
|
||||||
|
example: Europe/Copenhagen
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
'404':
|
'404':
|
||||||
$ref: '#/components/responses/NotFound'
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ namespace routes;
|
|||||||
|
|
||||||
use classes\authentication;
|
use classes\authentication;
|
||||||
use classes\selfserve;
|
use classes\selfserve;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeZone;
|
||||||
use objects\categories_o;
|
use objects\categories_o;
|
||||||
use objects\department_categories_o;
|
use objects\department_categories_o;
|
||||||
use objects\department_lanes_o;
|
use objects\department_lanes_o;
|
||||||
|
use objects\department_time_bookings_opening_hours_o;
|
||||||
use objects\department_variables_o;
|
use objects\department_variables_o;
|
||||||
use objects\departments_o;
|
use objects\departments_o;
|
||||||
use objects\logs_o;
|
use objects\logs_o;
|
||||||
@@ -330,9 +333,9 @@ class departmentsRoute
|
|||||||
$enabled = $department_variables->getVariable('selfserve_enabled');
|
$enabled = $department_variables->getVariable('selfserve_enabled');
|
||||||
|
|
||||||
// Return the status
|
// Return the status
|
||||||
$response->success([
|
$response->success(
|
||||||
'enabled' => $enabled === true
|
$this->buildDepartmentSelfServeEnabledPayload((int)$department->id, $enabled === true)
|
||||||
]);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Return an error
|
// Return an error
|
||||||
$response->error('Invalid session', 400);
|
$response->error('Invalid session', 400);
|
||||||
@@ -368,14 +371,22 @@ class departmentsRoute
|
|||||||
// Set the department variable
|
// Set the department variable
|
||||||
$department_variables = (new department_variables_o())->selectDepartment($department->id);
|
$department_variables = (new department_variables_o())->selectDepartment($department->id);
|
||||||
$enabled = self::getParameter('enabled') === 'true' || self::getParameter('enabled') === true || self::getParameter('enabled') === 1 || self::getParameter('enabled') === '1';
|
$enabled = self::getParameter('enabled') === 'true' || self::getParameter('enabled') === true || self::getParameter('enabled') === 1 || self::getParameter('enabled') === '1';
|
||||||
$department_variables->set('selfserve_enabled', $enabled ? 'true' : 'false');
|
department_variables_o::withSelfServeTransitionLock(
|
||||||
$this->syncDepartmentSelfServeRelayStates((int)$department->id, $enabled);
|
(int)$department->id,
|
||||||
|
function () use ($department_variables, $department, $enabled): void {
|
||||||
|
$department_variables->set('selfserve_enabled', $enabled ? 'true' : 'false');
|
||||||
|
$this->syncDepartmentSelfServeRelayStates((int)$department->id, $enabled);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Log the incident
|
// Log the incident
|
||||||
(new logs_o())->add('departments', $department->id, 1, $user->id, 'EDIT_DEPARTMENT_SELFSERVE_ENABLED', 'Successfully edited department self-serve enabled status to ' . ($enabled ? 'true' : 'false'));
|
(new logs_o())->add('departments', $department->id, 1, $user->id, 'EDIT_DEPARTMENT_SELFSERVE_ENABLED', 'Successfully edited department self-serve enabled status to ' . ($enabled ? 'true' : 'false'));
|
||||||
|
|
||||||
// Return a success message
|
// Return a success message
|
||||||
$response->success(['message' => 'Department self-serve enabled status updated successfully']);
|
$response->success([
|
||||||
|
'message' => 'Department self-serve enabled status updated successfully',
|
||||||
|
...$this->buildDepartmentSelfServeEnabledPayload((int)$department->id, $enabled)
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Return an error
|
// Return an error
|
||||||
$response->error('Invalid session', 400);
|
$response->error('Invalid session', 400);
|
||||||
@@ -593,13 +604,13 @@ class departmentsRoute
|
|||||||
if (!$enabled) {
|
if (!$enabled) {
|
||||||
// Self-serve disabled: restore normal/manual relay operation.
|
// Self-serve disabled: restore normal/manual relay operation.
|
||||||
$this->setOptionalLaneRelayState($lane, 'relay_machine_program_picker_id', static function () use ($lane): void {
|
$this->setOptionalLaneRelayState($lane, 'relay_machine_program_picker_id', static function () use ($lane): void {
|
||||||
$lane->setMachineProgramPickerRelayStatusHard(true);
|
$lane->setMachineProgramPickerRelayStatusForDepartmentOperation(true);
|
||||||
});
|
});
|
||||||
$this->setOptionalLaneRelayState($lane, 'relay_machine_cleaner_id', static function () use ($lane): void {
|
$this->setOptionalLaneRelayState($lane, 'relay_machine_cleaner_id', static function () use ($lane): void {
|
||||||
$lane->setMachineCleanerRelayStatusHard(true);
|
$lane->setMachineCleanerRelayStatusForDepartmentOperation(true);
|
||||||
});
|
});
|
||||||
$this->setOptionalLaneRelayState($lane, 'relay_machine_id', static function () use ($lane): void {
|
$this->setOptionalLaneRelayState($lane, 'relay_machine_id', static function () use ($lane): void {
|
||||||
$lane->setMachineRelayStatusHard(true);
|
$lane->setMachineRelayStatusForDepartmentOperation(true);
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -643,4 +654,38 @@ class departmentsRoute
|
|||||||
// Best effort only; this endpoint should still update the department variable.
|
// Best effort only; this endpoint should still update the department variable.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildDepartmentSelfServeEnabledPayload(int $departmentId, bool $enabled): array
|
||||||
|
{
|
||||||
|
$nextDeactivationAt = $enabled
|
||||||
|
? $this->getDepartmentSelfServeAutoDeactivationAt($departmentId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'enabled' => $enabled,
|
||||||
|
'auto_deactivation' => [
|
||||||
|
'at' => $nextDeactivationAt?->format(DATE_ATOM),
|
||||||
|
'timezone' => 'Europe/Copenhagen',
|
||||||
|
'label' => $nextDeactivationAt === null
|
||||||
|
? 'NEVER'
|
||||||
|
: $nextDeactivationAt->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDepartmentSelfServeAutoDeactivationAt(int $departmentId): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$openingHours = new department_time_bookings_opening_hours_o();
|
||||||
|
if (!$openingHours->selectExistingByDepartment($departmentId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $openingHours->getNextOpeningStart(
|
||||||
|
new DateTimeImmutable('now', new DateTimeZone('Europe/Copenhagen'))
|
||||||
|
);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-3
@@ -6,16 +6,28 @@ it('syncs lane relay states when department self-serve enabled flag changes', fu
|
|||||||
expect($routeContent)->not->toBeFalse();
|
expect($routeContent)->not->toBeFalse();
|
||||||
expect($routeContent)->toContain('/departments/self-serve/enabled');
|
expect($routeContent)->toContain('/departments/self-serve/enabled');
|
||||||
expect($routeContent)->toContain('$this->syncDepartmentSelfServeRelayStates((int)$department->id, $enabled);');
|
expect($routeContent)->toContain('$this->syncDepartmentSelfServeRelayStates((int)$department->id, $enabled);');
|
||||||
|
expect($routeContent)->toContain('department_variables_o::withSelfServeTransitionLock');
|
||||||
expect($routeContent)->toContain('if (!$enabled) {');
|
expect($routeContent)->toContain('if (!$enabled) {');
|
||||||
expect($routeContent)->toContain('// Self-serve disabled: restore normal/manual relay operation.');
|
expect($routeContent)->toContain('// Self-serve disabled: restore normal/manual relay operation.');
|
||||||
expect($routeContent)->toContain('setMachineProgramPickerRelayStatusHard(true)');
|
expect($routeContent)->toContain('setMachineProgramPickerRelayStatusForDepartmentOperation(true)');
|
||||||
expect($routeContent)->toContain('setMachineRelayStatusHard(true)');
|
expect($routeContent)->toContain('setMachineRelayStatusForDepartmentOperation(true)');
|
||||||
expect($routeContent)->toContain('!$department_lane->isSelfServeEnabled()');
|
expect($routeContent)->toContain('!$department_lane->isSelfServeEnabled()');
|
||||||
expect($routeContent)->toContain('setMachineProgramPickerRelayStatus(false)');
|
expect($routeContent)->toContain('setMachineProgramPickerRelayStatus(false)');
|
||||||
expect($routeContent)->toContain('setMachineCleanerRelayStatusHard(true)');
|
expect($routeContent)->toContain('setMachineCleanerRelayStatusForDepartmentOperation(true)');
|
||||||
expect($routeContent)->toContain('setMachineRelayStatus(false)');
|
expect($routeContent)->toContain('setMachineRelayStatus(false)');
|
||||||
expect($routeContent)->not->toContain('setMachineProgramPickerRelayStatusHard(false)');
|
expect($routeContent)->not->toContain('setMachineProgramPickerRelayStatusHard(false)');
|
||||||
expect($routeContent)->not->toContain('setMachineCleanerRelayStatusHard(false)');
|
expect($routeContent)->not->toContain('setMachineCleanerRelayStatusHard(false)');
|
||||||
expect($routeContent)->not->toContain('setMachineRelayStatusHard(false)');
|
expect($routeContent)->not->toContain('setMachineRelayStatusHard(false)');
|
||||||
expect($routeContent)->not->toContain('setMachineCleanerRelayStatus(false)');
|
expect($routeContent)->not->toContain('setMachineCleanerRelayStatus(false)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the supported prepared database API for the department transition lock', function (): void {
|
||||||
|
$variableObject = file_get_contents(app_path('objects/department_variables_o.php'));
|
||||||
|
|
||||||
|
expect($variableObject)->not->toBeFalse()
|
||||||
|
->and($variableObject)->not->toContain('$database->selectOne')
|
||||||
|
->and($variableObject)->toContain("\$db->prepare('SELECT GET_LOCK(?, ?) AS acquired')")
|
||||||
|
->and($variableObject)->toContain("\$statement->bind_param('si', \$lockName, \$timeoutSeconds)")
|
||||||
|
->and($variableObject)->toContain("\$db->prepare('SELECT RELEASE_LOCK(?) AS released')")
|
||||||
|
->and($variableObject)->toContain("\$statement->bind_param('s', \$lockName)");
|
||||||
|
});
|
||||||
|
|||||||
+38
-6
@@ -7,10 +7,12 @@ it('registers an opening-time self-serve cleaner relay activation cron', functio
|
|||||||
expect($tasks[0]['id'])->toBe('selfserve.activate_opening_cleaner_relays');
|
expect($tasks[0]['id'])->toBe('selfserve.activate_opening_cleaner_relays');
|
||||||
expect($tasks[0]['legacy_name'])->toBe('SelfserveOpeningRelayActivationCron');
|
expect($tasks[0]['legacy_name'])->toBe('SelfserveOpeningRelayActivationCron');
|
||||||
expect($tasks[0]['handler'])->toBe('SelfserveOpeningRelayActivationCron');
|
expect($tasks[0]['handler'])->toBe('SelfserveOpeningRelayActivationCron');
|
||||||
|
expect($tasks[0]['name'])->toContain('disable department self-serve');
|
||||||
|
expect($tasks[0]['description'])->toContain('every configured lane relay');
|
||||||
expect($tasks[0]['schedule'])->toBe(['type' => 'interval', 'seconds' => 60]);
|
expect($tasks[0]['schedule'])->toBe(['type' => 'interval', 'seconds' => 60]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('activates only configured cleaner relays after department opening time', function (): void {
|
it('disables department self-serve and restores staffed relay defaults after department opening time', function (): void {
|
||||||
$cronContent = file_get_contents(app_path('cron/Cron.php'));
|
$cronContent = file_get_contents(app_path('cron/Cron.php'));
|
||||||
|
|
||||||
expect($cronContent)->not->toBeFalse();
|
expect($cronContent)->not->toBeFalse();
|
||||||
@@ -18,11 +20,41 @@ it('activates only configured cleaner relays after department opening time', fun
|
|||||||
expect($cronContent)->toContain("new DateTimeZone('Europe/Copenhagen')");
|
expect($cronContent)->toContain("new DateTimeZone('Europe/Copenhagen')");
|
||||||
expect($cronContent)->toContain('department_time_bookings_opening_hours');
|
expect($cronContent)->toContain('department_time_bookings_opening_hours');
|
||||||
expect($cronContent)->toContain("dv.variable = 'selfserve_enabled'");
|
expect($cronContent)->toContain("dv.variable = 'selfserve_enabled'");
|
||||||
expect($cronContent)->toContain('dl.selfserve_enabled');
|
expect($cronContent)->toContain("->set('selfserve_enabled', 'false')");
|
||||||
expect($cronContent)->toContain('relay_machine_cleaner_id');
|
expect($cronContent)->toContain('use objects\department_variables_o;');
|
||||||
|
expect($cronContent)->toContain('selfserveActivateStaffedDefaultRelaysForDepartment');
|
||||||
expect($cronContent)->toContain('$nowSeconds >= $opensAtSeconds && $nowSeconds < $closesAtSeconds');
|
expect($cronContent)->toContain('$nowSeconds >= $opensAtSeconds && $nowSeconds < $closesAtSeconds');
|
||||||
expect($cronContent)->toContain('setMachineCleanerRelayStatusHard(true)');
|
expect($cronContent)->toContain('selfserveOpeningCleanerRelayOvernightCarryoverActive');
|
||||||
|
expect($cronContent)->toContain('$previousDate = $now->modify(\'-1 day\')');
|
||||||
|
expect($cronContent)->not->toContain('if (selfserveOpeningCleanerRelayActivationAlreadyHandled($cacheKey))');
|
||||||
|
expect($cronContent)->not->toContain('if (!$departmentLane->isSelfServeEnabled())');
|
||||||
|
expect($cronContent)->toContain('throw $throwable;');
|
||||||
|
expect($cronContent)->toContain('department_variables_o::withSelfServeTransitionLock');
|
||||||
|
expect(strpos($cronContent, 'selfserveActivateStaffedDefaultRelaysForDepartment($departmentId)'))
|
||||||
|
->toBeLessThan(strpos($cronContent, 'selfserveDisableDepartmentSelfServeForOpening($departmentId)'));
|
||||||
|
expect($cronContent)->toContain('setMachineProgramPickerRelayStatusForDepartmentOperation(true)');
|
||||||
|
expect($cronContent)->toContain('setMachineCleanerRelayStatusForDepartmentOperation(true)');
|
||||||
|
expect($cronContent)->toContain('setMachineRelayStatusForDepartmentOperation(true)');
|
||||||
expect($cronContent)->toContain('selfserve:opening-cleaner-relays:');
|
expect($cronContent)->toContain('selfserve:opening-cleaner-relays:');
|
||||||
expect($cronContent)->not->toContain('setMachineRelayStatusHard(true)');
|
});
|
||||||
expect($cronContent)->not->toContain('setMachineProgramPickerRelayStatusHard(true)');
|
|
||||||
|
it('exposes next self-serve auto deactivation metadata from the department endpoint', function (): void {
|
||||||
|
$routeContent = file_get_contents(app_path('routes/departmentsRoute.php'));
|
||||||
|
$openingHoursContent = file_get_contents(app_path('objects/department_time_bookings_opening_hours_o.php'));
|
||||||
|
|
||||||
|
expect($routeContent)->not->toBeFalse();
|
||||||
|
expect($openingHoursContent)->not->toBeFalse();
|
||||||
|
expect($routeContent)->toContain('buildDepartmentSelfServeEnabledPayload');
|
||||||
|
expect($routeContent)->toContain('selectExistingByDepartment($departmentId)');
|
||||||
|
expect($routeContent)->toContain("'auto_deactivation'");
|
||||||
|
expect($routeContent)->toContain("'label' => \$nextDeactivationAt === null");
|
||||||
|
expect($routeContent)->toContain("'NEVER'");
|
||||||
|
expect($openingHoursContent)->toContain('function getNextOpeningStart(DateTimeImmutable $now): ?DateTimeImmutable');
|
||||||
|
expect($openingHoursContent)->toContain('$openingStart === null');
|
||||||
|
expect($openingHoursContent)->toContain('$openingEnd === null');
|
||||||
|
expect($openingHoursContent)->toContain('$openingStart === $openingEnd');
|
||||||
|
expect($openingHoursContent)->toContain('$dayOffset <= 7');
|
||||||
|
expect($openingHoursContent)->toContain('$previousWeekdayName');
|
||||||
|
expect($openingHoursContent)->toContain('return $now;');
|
||||||
|
expect(file_get_contents(app_path('openapi.yaml')))->toContain('auto_deactivation:');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user