Activate self-serve opening relays

Activate self-serve opening relays
This commit is contained in:
Jeppe B
2026-07-09 11:27:32 +02:00
committed by GitHub
parent 6a00f023b1
commit 7a1c444df0
5 changed files with 346 additions and 9 deletions
+276
View File
@@ -8,6 +8,7 @@ use classes\invoice_period_flag_service;
use classes\coolify_manager; use classes\coolify_manager;
use classes\replication_manager; use classes\replication_manager;
use classes\redis; use classes\redis;
use classes\selfserve;
use classes\system_search_cache; use classes\system_search_cache;
use classes\system_search_document_index; use classes\system_search_document_index;
use classes\system_search_economic_customer_index; use classes\system_search_economic_customer_index;
@@ -1474,6 +1475,281 @@ function goalsProgressAlertDue(goals_criteria $criteria, DateTimeImmutable $nowU
} }
} }
function SelfserveOpeningRelayActivationCron(): array
{
global $db;
if (!($db instanceof \classes\db)) {
warn('SelfserveOpeningRelayActivationCron skipped: database connection is unavailable.');
return [
'skipped' => true,
'reason' => 'database_unavailable',
];
}
$now = new DateTimeImmutable('now', new DateTimeZone('Europe/Copenhagen'));
$candidates = selfserveOpeningCleanerRelayActivationCandidates($now);
$summary = [
'checked_departments' => count($candidates),
'activated_departments' => 0,
'skipped_departments' => 0,
'failed_departments' => 0,
'activated_relays' => 0,
'skipped_relays' => 0,
'failed_relays' => 0,
];
foreach ($candidates as $candidate) {
$departmentId = (int)($candidate['department_id'] ?? 0);
$opensAt = (string)($candidate['opens_at'] ?? '');
if ($departmentId <= 0 || $opensAt === '') {
$summary['skipped_departments']++;
continue;
}
$cacheKey = selfserveOpeningCleanerRelayActivationKey($departmentId, $opensAt, $now);
if (selfserveOpeningCleanerRelayActivationAlreadyHandled($cacheKey)) {
$summary['skipped_departments']++;
continue;
}
$departmentSummary = selfserveActivateOpeningCleanerRelaysForDepartment($departmentId);
$summary['activated_relays'] += (int)$departmentSummary['activated'];
$summary['skipped_relays'] += (int)$departmentSummary['skipped'];
$summary['failed_relays'] += (int)$departmentSummary['failed'];
if ((int)$departmentSummary['failed'] > 0) {
$summary['failed_departments']++;
continue;
}
selfserveMarkOpeningCleanerRelayActivationHandled($cacheKey);
if ((int)$departmentSummary['activated'] > 0) {
$summary['activated_departments']++;
} else {
$summary['skipped_departments']++;
}
}
echo "[" . date('Y-m-d H:i:s') . "][CRON] SelfserveOpeningRelayActivationCron: "
. $summary['activated_relays'] . " cleaner relays activated across "
. $summary['activated_departments'] . " departments.\n";
return $summary;
}
function selfserveOpeningCleanerRelayActivationCandidates(DateTimeImmutable $now): array
{
global $db;
$weekday = strtolower($now->format('l'));
$allowedWeekdays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
if (!in_array($weekday, $allowedWeekdays, true)) {
return [];
}
$startColumn = $weekday . '_start';
$endColumn = $weekday . '_end';
$sql = "
SELECT
oh.department AS department_id,
TIME_FORMAT(oh.`$startColumn`, '%H:%i:%s') AS opens_at,
TIME_FORMAT(oh.`$endColumn`, '%H:%i:%s') AS closes_at
FROM department_time_bookings_opening_hours oh
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'
AND LOWER(TRIM(COALESCE(dv.value, ''))) IN ('true', '1', 'yes', 'on')
AND oh.`$startColumn` IS NOT NULL
AND oh.`$endColumn` IS NOT NULL
AND dl.deleted_at IS NULL
AND COALESCE(dl.selfserve_enabled, 1) = 1
AND dl.relay_machine_cleaner_id IS NOT NULL
AND TRIM(dl.relay_machine_cleaner_id) <> ''
GROUP BY oh.department, oh.`$startColumn`, oh.`$endColumn`
";
$result = $db->query($sql);
if (!$result instanceof mysqli_result) {
return [];
}
return array_values(array_filter(
$db->fetch_all($result),
static fn(array $row): bool => selfserveOpeningCleanerRelayWindowActive(
$now,
(string)($row['opens_at'] ?? ''),
(string)($row['closes_at'] ?? '')
)
));
}
function selfserveOpeningCleanerRelayWindowActive(
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');
if ($opensAtSeconds < $closesAtSeconds) {
return $nowSeconds >= $opensAtSeconds && $nowSeconds < $closesAtSeconds;
}
return $nowSeconds >= $opensAtSeconds;
}
function selfserveOpeningCleanerRelayTimeToSeconds(?string $time): ?int
{
$time = trim((string)$time);
if ($time === '') {
return null;
}
if (!preg_match('/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/', $time, $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 * 3600) + ($minutes * 60) + $seconds;
}
function selfserveActivateOpeningCleanerRelaysForDepartment(int $departmentId): array
{
$summary = [
'activated' => 0,
'skipped' => 0,
'failed' => 0,
];
$selfserve = new selfserve();
$lanes = (new department_lanes_o())->getDepartmentLanes($departmentId);
foreach ($lanes as $departmentLane) {
if (!($departmentLane instanceof department_lanes_o)) {
$summary['skipped']++;
continue;
}
try {
if (!$departmentLane->isSelfServeEnabled()) {
$summary['skipped']++;
continue;
}
} catch (Throwable) {
$summary['skipped']++;
continue;
}
if (!selfserveLaneHasConfiguredCleanerRelay($departmentLane)) {
$summary['skipped']++;
continue;
}
$laneId = (int)$departmentLane->id;
if ($laneId <= 0) {
$summary['skipped']++;
continue;
}
try {
$lane = $selfserve->lane($laneId);
if (!selfserveLaneHasConfiguredCleanerRelay($lane->department_lane)) {
$summary['skipped']++;
continue;
}
$lane->setMachineCleanerRelayStatusHard(true);
$summary['activated']++;
} catch (Throwable $throwable) {
$summary['failed']++;
warn(
'SelfserveOpeningRelayActivationCron failed for department '
. $departmentId
. ', lane '
. $laneId
. ': '
. $throwable->getMessage()
);
}
}
return $summary;
}
function selfserveLaneHasConfiguredCleanerRelay(?object $departmentLane): bool
{
if (
$departmentLane === null
|| !isset($departmentLane->relay_machine_cleaner_id)
|| !is_object($departmentLane->relay_machine_cleaner_id)
|| !method_exists($departmentLane->relay_machine_cleaner_id, 'value')
) {
return false;
}
try {
$relayId = trim((string)$departmentLane->relay_machine_cleaner_id->value());
} catch (Throwable) {
return false;
}
return $relayId !== '' && $relayId !== '0' && strtolower($relayId) !== 'null';
}
function selfserveOpeningCleanerRelayActivationKey(
int $departmentId,
string $opensAt,
DateTimeImmutable $now
): string {
$normalizedOpensAt = preg_replace('/[^0-9]/', '', $opensAt) ?: 'unknown';
return 'selfserve:opening-cleaner-relays:'
. $now->format('Y-m-d')
. ':'
. $departmentId
. ':'
. $normalizedOpensAt;
}
function selfserveOpeningCleanerRelayActivationAlreadyHandled(string $cacheKey): bool
{
if (!defined('redis')) {
return false;
}
try {
return redis->exists($cacheKey);
} catch (Throwable) {
return false;
}
}
function selfserveMarkOpeningCleanerRelayActivationHandled(string $cacheKey): void
{
if (!defined('redis')) {
return;
}
try {
redis->setEx($cacheKey, '1', 36 * 3600);
} catch (Throwable) {
// Redis idempotency should not block relay activation.
}
}
if (defined('CRON_LOAD_LEGACY_FUNCTIONS_ONLY') && CRON_LOAD_LEGACY_FUNCTIONS_ONLY) { if (defined('CRON_LOAD_LEGACY_FUNCTIONS_ONLY') && CRON_LOAD_LEGACY_FUNCTIONS_ONLY) {
return; return;
} }
@@ -0,0 +1,16 @@
<?php
return [
[
'id' => 'selfserve.activate_opening_cleaner_relays',
'legacy_name' => 'SelfserveOpeningRelayActivationCron',
'name' => 'Activate self-serve opening cleaner relays',
'description' => 'Turns configured self-serve cleaner relays on when a department enters opening hours.',
'module' => 'selfserve',
'handler' => 'SelfserveOpeningRelayActivationCron',
'schedule' => ['type' => 'interval', 'seconds' => 60],
'timeout_seconds' => 300,
'estimated_duration_ms' => 5000,
'priority' => 45,
],
];
+20 -7
View File
@@ -575,11 +575,6 @@ class departmentsRoute
} }
protected function syncDepartmentSelfServeRelayStates(int $departmentId, bool $enabled): void protected function syncDepartmentSelfServeRelayStates(int $departmentId, bool $enabled): void
{ {
if (!$enabled) {
// Self-serve disabled: do not mutate lane relay states.
return;
}
$selfserve = new selfserve(); $selfserve = new selfserve();
$lanes = (new department_lanes_o())->getDepartmentLanes($departmentId); $lanes = (new department_lanes_o())->getDepartmentLanes($departmentId);
@@ -595,12 +590,30 @@ class departmentsRoute
continue; continue;
} }
// Self-serve enabled: keep machine stack off. if (!$enabled) {
// Self-serve disabled: restore normal/manual relay operation.
$this->setOptionalLaneRelayState($lane, 'relay_machine_program_picker_id', static function () use ($lane): void {
$lane->setMachineProgramPickerRelayStatusHard(true);
});
$this->setOptionalLaneRelayState($lane, 'relay_machine_cleaner_id', static function () use ($lane): void {
$lane->setMachineCleanerRelayStatusHard(true);
});
$this->setOptionalLaneRelayState($lane, 'relay_machine_id', static function () use ($lane): void {
$lane->setMachineRelayStatusHard(true);
});
continue;
}
if (!$department_lane->isSelfServeEnabled()) {
continue;
}
// Self-serve enabled: lances must be usable; machine-only relays stay off.
$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->setMachineProgramPickerRelayStatus(false); $lane->setMachineProgramPickerRelayStatus(false);
}); });
$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->setMachineCleanerRelayStatus(false); $lane->setMachineCleanerRelayStatusHard(true);
}); });
try { try {
$lane->setMachineRelayStatus(false); $lane->setMachineRelayStatus(false);
@@ -7,11 +7,15 @@ it('syncs lane relay states when department self-serve enabled flag changes', fu
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('if (!$enabled) {'); expect($routeContent)->toContain('if (!$enabled) {');
expect($routeContent)->toContain('// Self-serve disabled: do not mutate lane relay states.'); expect($routeContent)->toContain('// Self-serve disabled: restore normal/manual relay operation.');
expect($routeContent)->toContain('setMachineProgramPickerRelayStatusHard(true)');
expect($routeContent)->toContain('setMachineRelayStatusHard(true)');
expect($routeContent)->toContain('!$department_lane->isSelfServeEnabled()');
expect($routeContent)->toContain('setMachineProgramPickerRelayStatus(false)'); expect($routeContent)->toContain('setMachineProgramPickerRelayStatus(false)');
expect($routeContent)->toContain('setMachineCleanerRelayStatus(false)'); expect($routeContent)->toContain('setMachineCleanerRelayStatusHard(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)');
}); });
@@ -0,0 +1,28 @@
<?php
it('registers an opening-time self-serve cleaner relay activation cron', function (): void {
$tasks = require app_path('modules/selfserve/cron/tasks.php');
expect($tasks)->toHaveCount(1);
expect($tasks[0]['id'])->toBe('selfserve.activate_opening_cleaner_relays');
expect($tasks[0]['legacy_name'])->toBe('SelfserveOpeningRelayActivationCron');
expect($tasks[0]['handler'])->toBe('SelfserveOpeningRelayActivationCron');
expect($tasks[0]['schedule'])->toBe(['type' => 'interval', 'seconds' => 60]);
});
it('activates only configured cleaner relays after department opening time', function (): void {
$cronContent = file_get_contents(app_path('cron/Cron.php'));
expect($cronContent)->not->toBeFalse();
expect($cronContent)->toContain('function SelfserveOpeningRelayActivationCron(): array');
expect($cronContent)->toContain("new DateTimeZone('Europe/Copenhagen')");
expect($cronContent)->toContain('department_time_bookings_opening_hours');
expect($cronContent)->toContain("dv.variable = 'selfserve_enabled'");
expect($cronContent)->toContain('dl.selfserve_enabled');
expect($cronContent)->toContain('relay_machine_cleaner_id');
expect($cronContent)->toContain('$nowSeconds >= $opensAtSeconds && $nowSeconds < $closesAtSeconds');
expect($cronContent)->toContain('setMachineCleanerRelayStatusHard(true)');
expect($cronContent)->toContain('selfserve:opening-cleaner-relays:');
expect($cronContent)->not->toContain('setMachineRelayStatusHard(true)');
expect($cronContent)->not->toContain('setMachineProgramPickerRelayStatusHard(true)');
});