query(); $write = (bool) $this->option('write'); $chunkSize = max(1, (int) $this->option('chunk')); $candidateCount = (clone $query)->count(); if ($candidateCount === 0) { $this->info('No provider connections matched the classification scope.'); return self::SUCCESS; } $tenantCounts = (clone $query) ->selectRaw('tenant_id, count(*) as aggregate') ->groupBy('tenant_id') ->pluck('aggregate', 'tenant_id') ->map(static fn (mixed $count): int => (int) $count) ->all(); $startedTenants = []; $classifiedCount = 0; $appliedCount = 0; $reviewRequiredCount = 0; $query ->with(['tenant', 'credential']) ->orderBy('id') ->chunkById($chunkSize, function ($connections) use ( $classifier, $stateProjector, $write, $tenantCounts, &$startedTenants, &$classifiedCount, &$appliedCount, &$reviewRequiredCount, ): void { foreach ($connections as $connection) { $classifiedCount++; $result = $classifier->classify( $connection, source: 'tenantpilot:provider-connections:classify', ); if ($result->reviewRequired) { $reviewRequiredCount++; } if (! $write) { continue; } $tenant = $connection->tenant; if (! $tenant instanceof Tenant) { $this->warn(sprintf('Skipping provider connection #%d without tenant context.', (int) $connection->getKey())); continue; } $tenantKey = (int) $tenant->getKey(); if (! array_key_exists($tenantKey, $startedTenants)) { $this->auditStart($tenant, $tenantCounts[$tenantKey] ?? 0); $startedTenants[$tenantKey] = true; } $connection = $this->applyClassification($connection, $result, $stateProjector); $this->auditApplied($tenant, $connection, $result); $appliedCount++; } }); if ($write) { $this->info(sprintf('Applied classifications: %d', $appliedCount)); } else { $this->info(sprintf('Dry-run classifications: %d', $classifiedCount)); } $this->info(sprintf('Review required: %d', $reviewRequiredCount)); $this->info(sprintf('Mode: %s', $write ? 'write' : 'dry-run')); return self::SUCCESS; } private function query(): Builder { $query = ProviderConnection::query() ->where('provider', (string) $this->option('provider')); $tenantOption = $this->option('tenant'); if (is_string($tenantOption) && trim($tenantOption) !== '') { $tenant = Tenant::query() ->forTenant(trim($tenantOption)) ->firstOrFail(); $query->where('tenant_id', (int) $tenant->getKey()); } $connectionOption = $this->option('connection'); if (is_numeric($connectionOption)) { $query->whereKey((int) $connectionOption); } return $query; } private function applyClassification( ProviderConnection $connection, ProviderConnectionClassificationResult $result, ProviderConnectionStateProjector $stateProjector, ): ProviderConnection { DB::transaction(function () use ($connection, $result, $stateProjector): void { $connection->forceFill( $connection->classificationProjection($result, $stateProjector) )->save(); $credential = $connection->credential; if (! $credential instanceof ProviderCredential) { return; } $updates = []; if ( $result->suggestedConnectionType === ProviderConnectionType::Dedicated && $credential->source === null ) { $updates['source'] = ProviderCredentialSource::LegacyMigrated->value; } if ($credential->credential_kind === null && $credential->type === ProviderCredentialKind::ClientSecret->value) { $updates['credential_kind'] = ProviderCredentialKind::ClientSecret->value; } if ($updates !== []) { $credential->forceFill($updates)->save(); } }); return $connection->fresh(['tenant', 'credential']); } private function auditStart(Tenant $tenant, int $candidateCount): void { app(AuditLogger::class)->log( tenant: $tenant, action: 'provider_connection.migration_classification_started', context: [ 'metadata' => [ 'source' => 'tenantpilot:provider-connections:classify', 'provider' => 'microsoft', 'candidate_count' => $candidateCount, 'write' => true, ], ], resourceType: 'tenant', resourceId: (string) $tenant->getKey(), status: 'success', ); } private function auditApplied( Tenant $tenant, ProviderConnection $connection, ProviderConnectionClassificationResult $result, ): void { $effectiveApp = $connection->effectiveAppMetadata(); app(AuditLogger::class)->log( tenant: $tenant, action: 'provider_connection.migration_classification_applied', context: [ 'metadata' => [ 'source' => 'tenantpilot:provider-connections:classify', 'workspace_id' => (int) $connection->workspace_id, 'provider_connection_id' => (int) $connection->getKey(), 'provider' => (string) $connection->provider, 'entra_tenant_id' => (string) $connection->entra_tenant_id, 'connection_type' => $connection->connection_type->value, 'migration_review_required' => $connection->migration_review_required, 'legacy_identity_result' => $result->suggestedConnectionType->value, 'effective_app_id' => $effectiveApp['app_id'], 'effective_app_source' => $effectiveApp['source'], 'signals' => $result->signals, ], ], resourceType: 'provider_connection', resourceId: (string) $connection->getKey(), status: 'success', ); } }