where('is_enabled', true) ->whereHas('tenant', fn ($query) => $query->where('status', 'active')) ->with('tenant'); if (is_array($tenantIdentifiers) && ! empty($tenantIdentifiers)) { $schedulesQuery->whereIn('tenant_id', $this->resolveTenantIds($tenantIdentifiers)); } $createdRuns = 0; $skippedRuns = 0; $scannedSchedules = 0; foreach ($schedulesQuery->cursor() as $schedule) { $scannedSchedules++; $slot = $this->scheduleTimeService->nextRunFor($schedule, $nowUtc->subMinute()); if ($slot === null) { $schedule->forceFill(['next_run_at' => null])->saveQuietly(); continue; } if ($slot->greaterThan($nowUtc)) { if (! $schedule->next_run_at || ! $schedule->next_run_at->equalTo($slot)) { $schedule->forceFill(['next_run_at' => $slot])->saveQuietly(); } continue; } $run = null; try { $run = BackupScheduleRun::create([ 'backup_schedule_id' => $schedule->id, 'tenant_id' => $schedule->tenant_id, 'scheduled_for' => $slot->toDateTimeString(), 'status' => BackupScheduleRun::STATUS_RUNNING, 'summary' => null, ]); } catch (UniqueConstraintViolationException) { // Idempotency: unique (backup_schedule_id, scheduled_for) $skippedRuns++; Log::debug('Backup schedule run already dispatched for slot.', [ 'schedule_id' => $schedule->id, 'slot' => $slot->toDateTimeString(), ]); $schedule->forceFill([ 'next_run_at' => $this->scheduleTimeService->nextRunFor($schedule, $nowUtc), ])->saveQuietly(); continue; } $createdRuns++; $this->auditLogger->log( tenant: $schedule->tenant, action: 'backup_schedule.run_dispatched', context: [ 'metadata' => [ 'backup_schedule_id' => $schedule->id, 'backup_schedule_run_id' => $run->id, 'scheduled_for' => $slot->toDateTimeString(), ], ], resourceType: 'backup_schedule_run', resourceId: (string) $run->id, status: 'success' ); $schedule->forceFill([ 'next_run_at' => $this->scheduleTimeService->nextRunFor($schedule, $nowUtc), ])->saveQuietly(); Bus::dispatch(new RunBackupScheduleJob($run->id)); } return [ 'created_runs' => $createdRuns, 'skipped_runs' => $skippedRuns, 'scanned_schedules' => $scannedSchedules, ]; } /** * @param array $tenantIdentifiers * @return array */ private function resolveTenantIds(array $tenantIdentifiers): array { $tenantIds = []; foreach ($tenantIdentifiers as $identifier) { $tenant = Tenant::query() ->where('status', 'active') ->forTenant($identifier) ->first(); if ($tenant) { $tenantIds[] = $tenant->id; } } return array_values(array_unique($tenantIds)); } }