*/ public array $policyIds, public string $backupName, public ?string $backupDescription = null, ?OperationRun $operationRun = null, ) { $this->operationRun = $operationRun; } public function middleware(): array { return [new TrackOperationRun]; } public function handle(OperationRunService $operationRunService): void { $tenant = Tenant::query()->find($this->tenantId); if (! $tenant instanceof Tenant) { throw new \RuntimeException('Tenant not found.'); } $user = User::query()->find($this->userId); if (! $user instanceof User) { throw new \RuntimeException('User not found.'); } $ids = collect($this->policyIds) ->map(static fn ($id): int => (int) $id) ->unique() ->sort() ->values() ->all(); try { // Create Backup Set $backupSet = BackupSet::create([ 'tenant_id' => $tenant->getKey(), 'name' => $this->backupName, // 'description' => $this->backupDescription, // Not in schema 'status' => 'completed', 'created_by' => $user->name, 'item_count' => count($ids), 'completed_at' => now(), ]); $itemCount = 0; $succeeded = 0; $failed = 0; $failures = []; $totalItems = count($ids); $failureThreshold = (int) floor($totalItems / 2); foreach ($ids as $policyId) { $itemCount++; try { $policy = Policy::query() ->where('tenant_id', $tenant->getKey()) ->find($policyId); if (! $policy) { $failed++; $failures[] = ['code' => 'policy.not_found', 'message' => "Policy {$policyId} not found."]; if ($failed > $failureThreshold) { $backupSet->update(['status' => 'failed']); if ($this->operationRun) { $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Failed->value, summaryCounts: [ 'total' => $totalItems, 'processed' => $itemCount, 'succeeded' => $succeeded, 'failed' => $failed, 'created' => $succeeded, ], failures: array_merge($failures, [ ['code' => 'export.circuit_breaker', 'message' => 'Circuit breaker: more than 50% of items failed.'], ]), ); } if ($user) { Notification::make() ->title('Bulk Export Aborted') ->body('Circuit breaker triggered: too many failures (>50%).') ->icon('heroicon-o-exclamation-triangle') ->danger() ->actions($this->operationRun ? [ \Filament\Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($this->operationRun, $tenant)), ] : []) ->sendToDatabase($user) ->send(); } return; } continue; } // Get latest version for snapshot $latestVersion = $policy->versions()->orderByDesc('captured_at')->first(); if (! $latestVersion) { $failed++; $failures[] = ['code' => 'policy.no_versions', 'message' => "No versions available for policy {$policyId}."]; if ($failed > $failureThreshold) { $backupSet->update(['status' => 'failed']); if ($this->operationRun) { $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Failed->value, summaryCounts: [ 'total' => $totalItems, 'processed' => $itemCount, 'succeeded' => $succeeded, 'failed' => $failed, 'created' => $succeeded, ], failures: array_merge($failures, [ ['code' => 'export.circuit_breaker', 'message' => 'Circuit breaker: more than 50% of items failed.'], ]), ); } if ($user) { Notification::make() ->title('Bulk Export Aborted') ->body('Circuit breaker triggered: too many failures (>50%).') ->icon('heroicon-o-exclamation-triangle') ->danger() ->actions($this->operationRun ? [ \Filament\Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($this->operationRun, $tenant)), ] : []) ->sendToDatabase($user) ->send(); } return; } continue; } // Create Backup Item BackupItem::create([ 'tenant_id' => $tenant->getKey(), 'backup_set_id' => $backupSet->id, 'policy_id' => $policy->id, 'policy_identifier' => $policy->external_id, // Added 'policy_type' => $policy->policy_type, 'platform' => $policy->platform ?? null, // Added // 'display_name' => $policy->display_name, // Not in schema, maybe in metadata? 'payload' => $latestVersion->snapshot, // Mapped to payload 'metadata' => [ 'display_name' => $policy->display_name, // Stored in metadata 'version_captured_at' => $latestVersion->captured_at->toIso8601String(), ], ]); $succeeded++; } catch (Throwable $e) { $failed++; $failures[] = ['code' => 'policy.export.failed', 'message' => $e->getMessage()]; if ($failed > $failureThreshold) { $backupSet->update(['status' => 'failed']); if ($this->operationRun) { $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Failed->value, summaryCounts: [ 'total' => $totalItems, 'processed' => $itemCount, 'succeeded' => $succeeded, 'failed' => $failed, 'created' => $succeeded, ], failures: array_merge($failures, [ ['code' => 'export.circuit_breaker', 'message' => 'Circuit breaker: more than 50% of items failed.'], ]), ); } if ($user) { Notification::make() ->title('Bulk Export Aborted') ->body('Circuit breaker triggered: too many failures (>50%).') ->icon('heroicon-o-exclamation-triangle') ->danger() ->actions($this->operationRun ? [ \Filament\Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($this->operationRun, $tenant)), ] : []) ->sendToDatabase($user) ->send(); } return; } } } // Update BackupSet item count (if denormalized) or just leave it // Assuming BackupSet might need an item count or status update $outcome = OperationRunOutcome::Succeeded->value; if ($failed > 0 && $failed < $totalItems) { $outcome = OperationRunOutcome::PartiallySucceeded->value; } if ($failed >= $totalItems && $totalItems > 0) { $outcome = OperationRunOutcome::Failed->value; } if ($this->operationRun) { $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: $outcome, summaryCounts: [ 'total' => $totalItems, 'processed' => $totalItems, 'succeeded' => $succeeded, 'failed' => $failed, 'created' => $succeeded, ], failures: $failures, ); } if ($succeeded > 0 || $failed > 0) { $message = "Successfully exported {$succeeded} policies to backup '{$this->backupName}'"; if ($failed > 0) { $message .= " ({$failed} failed)"; } $message .= '.'; Notification::make() ->title('Bulk Export Completed') ->body($message) ->icon('heroicon-o-check-circle') ->success() ->actions($this->operationRun ? [ \Filament\Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($this->operationRun, $tenant)), ] : []) ->sendToDatabase($user) ->send(); } } catch (Throwable $e) { if ($this->operationRun) { $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Failed->value, failures: [ ['code' => 'exception.unhandled', 'message' => $e->getMessage()], ], ); } if (isset($user) && $user instanceof User) { Notification::make() ->title('Bulk Export Failed') ->body($e->getMessage()) ->icon('heroicon-o-x-circle') ->danger() ->actions($this->operationRun ? [ \Filament\Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($this->operationRun, $tenant)), ] : []) ->sendToDatabase($user) ->send(); } throw $e; } } }