Activate self-serve opening relays
Activate self-serve opening relays
This commit is contained in:
@@ -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,
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -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);
|
||||||
|
|||||||
+6
-2
@@ -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)');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user