comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); Artisan::command('learning:process-schedules', function (CoursePublication $publication) { $published = 0; $unpublished = 0; CourseVersion::query()->where('status', CourseVersionStatus::InReview)->whereNotNull('scheduled_publish_at')->where('scheduled_publish_at', '<=', now())->each(function (CourseVersion $version) use ($publication, &$published) { $publication->publish($version); $published++; }); CourseVersion::query()->where('status', CourseVersionStatus::Published)->whereNull('unpublished_at')->whereNotNull('scheduled_unpublish_at')->where('scheduled_unpublish_at', '<=', now())->each(function (CourseVersion $version) use ($publication, &$unpublished) { $publication->unpublish($version); $unpublished++; }); $this->info("Published {$published}; unpublished {$unpublished}."); })->purpose('Process scheduled course publication lifecycle'); Schedule::command('learning:process-schedules')->everyMinute()->withoutOverlapping(); Artisan::command('notifications:process-schedules', function (NotificationOrchestrator $notifications) { $result = $notifications->deliverDue(); $this->info("Notification schedules: {$result['sent']} sent, {$result['skipped']} skipped, {$result['failed']} failed."); })->purpose('Deliver due in-app notification schedules'); Schedule::command('notifications:process-schedules')->everyMinute()->withoutOverlapping(); Artisan::command('system:heartbeat', function () { Cache::put('system:scheduler-heartbeat', now(), now()->addMinutes(5)); $this->info('Scheduler heartbeat recorded.'); }); Artisan::command('exports:prune', function () { $cutoff = now()->subDays((int) config('exports.retention_days', 30)); DB::table('export_jobs')->where('created_at', '<', $cutoff)->whereIn('status', ['completed', 'failed', 'cancelled'])->orderBy('created_at')->each(function ($row): void { if ($row->path && $row->disk) { Storage::disk($row->disk)->delete($row->path); } DB::table('export_jobs')->where('id', $row->id)->delete(); }); $this->info('Expired export artifacts pruned.'); }); Schedule::command('system:heartbeat')->everyMinute(); Schedule::job(new QueueHeartbeat)->everyMinute()->withoutOverlapping(); Schedule::command('exports:prune')->dailyAt('02:30')->withoutOverlapping();