Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b686e578f7 |
@@ -8,6 +8,7 @@ use classes\invoice_period_flag_service;
|
||||
use classes\coolify_manager;
|
||||
use classes\replication_manager;
|
||||
use classes\redis;
|
||||
use classes\selfserve;
|
||||
use classes\system_search_cache;
|
||||
use classes\system_search_document_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) {
|
||||
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
|
||||
{
|
||||
if (!$enabled) {
|
||||
// Self-serve disabled: do not mutate lane relay states.
|
||||
return;
|
||||
}
|
||||
|
||||
$selfserve = new selfserve();
|
||||
$lanes = (new department_lanes_o())->getDepartmentLanes($departmentId);
|
||||
|
||||
@@ -595,12 +590,30 @@ class departmentsRoute
|
||||
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 {
|
||||
$lane->setMachineProgramPickerRelayStatus(false);
|
||||
});
|
||||
$this->setOptionalLaneRelayState($lane, 'relay_machine_cleaner_id', static function () use ($lane): void {
|
||||
$lane->setMachineCleanerRelayStatus(false);
|
||||
$lane->setMachineCleanerRelayStatusHard(true);
|
||||
});
|
||||
try {
|
||||
$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('$this->syncDepartmentSelfServeRelayStates((int)$department->id, $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('setMachineCleanerRelayStatus(false)');
|
||||
expect($routeContent)->toContain('setMachineCleanerRelayStatusHard(true)');
|
||||
expect($routeContent)->toContain('setMachineRelayStatus(false)');
|
||||
expect($routeContent)->not->toContain('setMachineProgramPickerRelayStatusHard(false)');
|
||||
expect($routeContent)->not->toContain('setMachineCleanerRelayStatusHard(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