$backupItemIds */ public function __construct( public int $backupSetId, public array $backupItemIds, public int $initiatorUserId, ?OperationRun $operationRun = null, ) { $this->operationRun = $operationRun; } /** * @return array */ public function middleware(): array { return [new TrackOperationRun]; } public function handle( AuditLogger $auditLogger, BulkOperationService $bulkOperationService, ): void { $backupSet = BackupSet::query()->with(['tenant'])->find($this->backupSetId); if (! $backupSet instanceof BackupSet) { if ($this->operationRun) { /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opService->updateRun( $this->operationRun, 'completed', 'failed', ['backup_set_id' => $this->backupSetId], [['code' => 'backup_set.not_found', 'message' => 'Backup set not found.']] ); } return; } $tenant = $backupSet->tenant; $initiator = User::query()->find($this->initiatorUserId); $requestedIds = collect($this->backupItemIds) ->map(fn (mixed $value): int => (int) $value) ->filter(fn (int $value): bool => $value > 0) ->unique() ->sort() ->values() ->all(); $requestedCount = count($requestedIds); $failures = []; try { /** @var \Illuminate\Database\Eloquent\Collection $items */ $items = BackupItem::query() ->where('backup_set_id', $backupSet->getKey()) ->whereIn('id', $requestedIds) ->get(); $foundIds = $items->pluck('id')->map(fn (mixed $value): int => (int) $value)->all(); $missingIds = array_values(array_diff($requestedIds, $foundIds)); foreach ($missingIds as $missingId) { $failures[] = [ 'code' => 'backup_item.not_found', 'message' => $bulkOperationService->sanitizeFailureReason("Backup item {$missingId} not found (already removed?)."), ]; } $removed = 0; $policyIds = []; $policyIdentifiers = []; foreach ($items as $item) { $item->delete(); $removed++; if ($item->policy_id) { $policyIds[] = (int) $item->policy_id; } if ($item->policy_identifier) { $policyIdentifiers[] = (string) $item->policy_identifier; } } $backupSet->update([ 'item_count' => $backupSet->items()->count(), ]); if ($tenant instanceof Tenant) { $auditLogger->log( tenant: $tenant, action: 'backup.items_removed', resourceType: 'backup_set', resourceId: (string) $backupSet->getKey(), status: 'success', context: [ 'metadata' => [ 'removed_count' => $removed, 'requested_count' => $requestedCount, 'missing_count' => count($missingIds), 'policy_ids' => array_values(array_unique($policyIds)), 'policy_identifiers' => array_values(array_unique($policyIdentifiers)), 'backup_set_id' => (int) $backupSet->getKey(), 'initiator_user_id' => $initiator?->getKey(), ], ], actorId: $initiator?->getKey(), ); } if ($this->operationRun) { /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $outcome = 'succeeded'; if ($removed === 0) { $outcome = 'failed'; } elseif ($failures !== []) { $outcome = 'partially_succeeded'; } $opService->updateRun( $this->operationRun, 'completed', $outcome, [ 'backup_set_id' => (int) $backupSet->getKey(), 'requested' => $requestedCount, 'removed' => $removed, 'missing' => count($missingIds), 'remaining' => (int) $backupSet->item_count, ], $failures, ); } $this->notifyCompleted( initiator: $initiator, tenant: $tenant instanceof Tenant ? $tenant : null, removed: $removed, requested: $requestedCount, missing: count($missingIds), outcome: $outcome, ); } catch (Throwable $throwable) { if ($tenant instanceof Tenant) { $auditLogger->log( tenant: $tenant, action: 'backup.items_removed', resourceType: 'backup_set', resourceId: (string) $backupSet->getKey(), status: 'failed', context: [ 'metadata' => [ 'requested_count' => $requestedCount, 'backup_set_id' => (int) $backupSet->getKey(), ], ], actorId: $initiator?->getKey(), ); } if ($this->operationRun) { /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opService->failRun($this->operationRun, $throwable); } $this->notifyFailed( initiator: $initiator, tenant: $tenant instanceof Tenant ? $tenant : null, reason: $bulkOperationService->sanitizeFailureReason($throwable->getMessage()), ); throw $throwable; } } private function notifyCompleted( ?User $initiator, ?Tenant $tenant, int $removed, int $requested, int $missing, ?string $outcome, ): void { if (! $initiator instanceof User) { return; } if (! $this->operationRun) { return; } $message = "Removed {$removed} policies"; if ($missing > 0) { $message .= " ({$missing} missing)"; } if ($requested !== $removed && $missing === 0) { $skipped = max(0, $requested - $removed); if ($skipped > 0) { $message .= " ({$skipped} not removed)"; } } $message .= '.'; $partial = in_array((string) $outcome, ['partially_succeeded'], true) || $missing > 0; $failed = in_array((string) $outcome, ['failed'], true); $notification = Notification::make() ->title($failed ? 'Removal failed' : ($partial ? 'Removal completed (partial)' : 'Removal completed')) ->body($message); if ($tenant instanceof Tenant) { $notification->actions([ \Filament\Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($this->operationRun, $tenant)), ]); } if ($failed) { $notification->danger(); } elseif ($partial) { $notification->warning(); } else { $notification->success(); } $notification ->sendToDatabase($initiator) ->send(); } private function notifyFailed(?User $initiator, ?Tenant $tenant, string $reason): void { if (! $initiator instanceof User) { return; } if (! $this->operationRun) { return; } $notification = Notification::make() ->title('Removal failed') ->body($reason); if ($tenant instanceof Tenant) { $notification->actions([ \Filament\Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($this->operationRun, $tenant)), ]); } $notification ->danger() ->sendToDatabase($initiator) ->send(); } }