resolveTenants(); if ($tenants->isEmpty()) { $this->error('No tenants selected. Provide {tenant} or use --all.'); return self::FAILURE; } $isDryRun = ! (bool) $this->option('force'); if ($isDryRun) { $this->warn('Dry run: no rows will be deleted. Re-run with --force to apply.'); } else { $this->warn('This will PERMANENTLY delete non-persistent tenant data.'); if ($this->input->isInteractive() && ! $this->confirm('Proceed?', false)) { $this->info('Aborted.'); return self::SUCCESS; } } foreach ($tenants as $tenant) { $counts = $this->countsForTenant($tenant); $this->line(''); $this->info("Tenant: {$tenant->id} ({$tenant->name})"); $this->table( ['Table', 'Rows'], collect($counts) ->map(fn (int $count, string $table) => [$table, $count]) ->values() ->all(), ); if ($isDryRun) { continue; } DB::transaction(function () use ($tenant): void { BackupSchedule::query() ->where('tenant_id', $tenant->id) ->delete(); OperationRun::query() ->where('tenant_id', $tenant->id) ->delete(); AuditLog::query() ->where('tenant_id', $tenant->id) ->delete(); RestoreRun::withTrashed() ->where('tenant_id', $tenant->id) ->forceDelete(); BackupItem::withTrashed() ->where('tenant_id', $tenant->id) ->forceDelete(); BackupSet::withTrashed() ->where('tenant_id', $tenant->id) ->forceDelete(); PolicyVersion::withTrashed() ->where('tenant_id', $tenant->id) ->forceDelete(); Policy::query() ->where('tenant_id', $tenant->id) ->delete(); }); $this->recordPurgeOperationRun($tenant, $counts); $this->info('Purged.'); } return self::SUCCESS; } private function resolveTenants() { if ((bool) $this->option('all')) { return Tenant::query()->get(); } $tenantArg = $this->argument('tenant'); if ($tenantArg !== null && $tenantArg !== '') { $tenant = Tenant::query()->forTenant($tenantArg)->first(); return $tenant ? collect([$tenant]) : collect(); } try { return collect([Tenant::currentOrFail()]); } catch (RuntimeException) { return collect(); } } /** * @return array */ private function countsForTenant(Tenant $tenant): array { return [ 'backup_schedules' => BackupSchedule::query()->where('tenant_id', $tenant->id)->count(), 'operation_runs' => OperationRun::query()->where('tenant_id', $tenant->id)->count(), 'audit_logs' => AuditLog::query()->where('tenant_id', $tenant->id)->count(), 'restore_runs' => RestoreRun::withTrashed()->where('tenant_id', $tenant->id)->count(), 'backup_items' => BackupItem::withTrashed()->where('tenant_id', $tenant->id)->count(), 'backup_sets' => BackupSet::withTrashed()->where('tenant_id', $tenant->id)->count(), 'policy_versions' => PolicyVersion::withTrashed()->where('tenant_id', $tenant->id)->count(), 'policies' => Policy::query()->where('tenant_id', $tenant->id)->count(), ]; } /** * @param array $counts */ private function recordPurgeOperationRun(Tenant $tenant, array $counts): void { OperationRun::query()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'tenant_id' => (int) $tenant->id, 'user_id' => null, 'initiator_name' => 'System', 'type' => 'backup_schedule_purge', 'status' => 'completed', 'outcome' => 'succeeded', 'run_identity_hash' => hash('sha256', implode(':', [ (string) $tenant->id, 'backup_schedule_purge', now()->toISOString(), Str::uuid()->toString(), ])), 'summary_counts' => [ 'total' => array_sum($counts), 'processed' => array_sum($counts), 'succeeded' => array_sum($counts), 'failed' => 0, ], 'failure_summary' => [], 'context' => [ 'source' => 'tenantpilot:purge-nonpersistent', 'deleted_rows' => $counts, ], 'started_at' => now(), 'completed_at' => now(), ]); } }