> */ public static function jobs(): array { return [ 'integrations_refresh' => [ 'name' => 'Integration cache refresh', 'feature' => 'Integrations', 'command' => 'integrations:refresh', 'description' => 'Refreshes cached Parcel2Go, BamBuddy and dryer data. Etsy runs separately through etsy:sync; the 30-second cadence and withoutOverlapping(5) guard are unchanged.', 'default_frequency' => 'every_30_seconds', 'overlap_minutes' => 5, 'manual' => true, ], 'alerts_scan' => [ 'name' => 'Alert scanner', 'feature' => 'Notifications', 'command' => 'alerts:scan', 'description' => 'Publishes notifications for new orders, print progress, low filament and due shipments. Migrated from the previous hard-coded schedule entry — frequency and withoutOverlapping(5) behavior unchanged.', 'default_frequency' => 'every_30_seconds', 'overlap_minutes' => 5, 'manual' => true, ], 'etsy_sync' => [ 'name' => 'Etsy shop sync', 'feature' => 'Etsy', 'command' => 'etsy:sync', 'description' => 'Forces a fresh pull of Etsy receipts and active listings into the same IntegrationCache entries the Etsy page reads, independent of the bundled integrations:refresh job so it can be tuned or disabled on its own.', 'default_frequency' => 'every_15_minutes', 'overlap_minutes' => 5, 'manual' => true, ], 'etsy_token_refresh' => [ 'name' => 'Etsy token refresh', 'feature' => 'Etsy', 'command' => 'etsy:refresh-token', 'description' => 'Checks every five minutes by default and refreshes only when expiry is within ten minutes. Disabling or slowing this job can leave idle tokens expired; on-demand reads also refresh expired tokens.', 'default_frequency' => 'every_5_minutes', 'overlap_minutes' => 5, 'manual' => true, ], ]; } /** * @return array */ public static function frequencyOptions(): array { return [ 'disabled' => 'Disabled', 'every_30_seconds' => 'Every 30 seconds', 'every_5_minutes' => 'Every 5 minutes', 'every_10_minutes' => 'Every 10 minutes', 'every_15_minutes' => 'Every 15 minutes', 'every_30_minutes' => 'Every 30 minutes', 'every_45_minutes' => 'Every 45 minutes', 'hourly' => 'Hourly', 'daily_02' => 'Daily at 02:00', 'daily_08' => 'Daily at 08:00', ]; } public static function frequencyFor(string $jobKey): string { $job = self::jobs()[$jobKey] ?? null; $default = $job['default_frequency'] ?? 'disabled'; return self::runtimeFrequencyFor($jobKey, $default); } public static function settingKey(string $jobKey): string { return 'schedule.'.$jobKey.'.frequency'; } /** * @param array $frequencies */ public static function persistRuntimeFrequencies(array $frequencies): void { $payload = collect($frequencies) ->only(array_keys(self::jobs())) ->filter(fn (string $frequency): bool => array_key_exists($frequency, self::frequencyOptions())) ->all(); File::ensureDirectoryExists(dirname(self::runtimeSettingsPath())); $lock = fopen(self::runtimeSettingsPath().'.lock', 'c'); if ($lock === false) { throw new \RuntimeException('Unable to open schedule settings lock.'); } try { if (! flock($lock, LOCK_EX)) { throw new \RuntimeException('Unable to lock schedule settings.'); } $stored = File::exists(self::runtimeSettingsPath()) ? json_decode(File::get(self::runtimeSettingsPath()), true, 512, JSON_THROW_ON_ERROR) : []; File::replace(self::runtimeSettingsPath(), json_encode(array_replace(is_array($stored) ? $stored : [], $payload), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); } finally { flock($lock, LOCK_UN); fclose($lock); } } public static function register(): void { foreach (self::jobs() as $jobKey => $job) { $frequency = self::frequencyFor($jobKey); if ($frequency === 'disabled' || ! is_string($job['command'] ?? null)) { continue; } $event = self::applyFrequency(Schedule::command($job['command']), $frequency); $overlap = $job['overlap_minutes'] ?? null; if ($overlap !== null) { $event->withoutOverlapping($overlap); } else { $event->withoutOverlapping(); } } } private static function runtimeFrequencyFor(string $jobKey, string $default): string { if (app()->environment('testing') && ! File::exists(self::runtimeSettingsPath())) { return $default; } try { if (! File::exists(self::runtimeSettingsPath())) { return $default; } $settings = json_decode((string) File::get(self::runtimeSettingsPath()), true, 512, JSON_THROW_ON_ERROR); } catch (Throwable) { return $default; } $stored = is_array($settings) ? ($settings[$jobKey] ?? $default) : $default; return is_string($stored) && array_key_exists($stored, self::frequencyOptions()) ? $stored : $default; } public static function runtimeSettingsPath(): string { if (app()->environment('testing')) { return storage_path('framework/testing/dashboard-schedules.json'); } return storage_path('app/dashboard-schedules.json'); } private static function applyFrequency(Event $event, string $frequency): Event { return match ($frequency) { 'every_30_seconds' => $event->everyThirtySeconds(), 'every_5_minutes' => $event->everyFiveMinutes(), 'every_10_minutes' => $event->everyTenMinutes(), 'every_15_minutes' => $event->everyFifteenMinutes(), 'every_30_minutes' => $event->everyThirtyMinutes(), 'every_45_minutes' => $event->cron('*/45 * * * *'), 'hourly' => $event->hourly(), 'daily_02' => $event->dailyAt('02:00'), 'daily_08' => $event->dailyAt('08:00'), default => $event->hourly(), }; } }