'boolean', 'connection_type' => ProviderConnectionType::class, 'consent_status' => ProviderConsentStatus::class, 'consent_granted_at' => 'datetime', 'consent_last_checked_at' => 'datetime', 'verification_status' => ProviderVerificationStatus::class, 'migration_review_required' => 'boolean', 'migration_reviewed_at' => 'datetime', 'scopes_granted' => 'array', 'metadata' => 'array', 'last_health_check_at' => 'datetime', ]; public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class); } public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } public function credential(): HasOne { return $this->hasOne(ProviderCredential::class, 'provider_connection_id'); } public function makeDefault(): void { DB::transaction(function (): void { static::query() ->where('tenant_id', $this->tenant_id) ->where('provider', $this->provider) ->where('is_default', true) ->whereKeyNot($this->getKey()) ->update(['is_default' => false]); static::query() ->whereKey($this->getKey()) ->update(['is_default' => true]); }); $this->refresh(); } /** * @return array{ * legacy_identity_classification_source: ?string, * legacy_identity_review_required: bool, * legacy_identity_result: ?string, * legacy_identity_signals: array, * legacy_identity_classified_at: ?string, * effective_app: array{app_id: ?string, source: string} * } */ public function legacyIdentityMetadata(): array { $metadata = is_array($this->metadata) ? $this->metadata : []; $effectiveApp = Arr::get($metadata, 'effective_app'); return [ 'legacy_identity_classification_source' => is_string($metadata['legacy_identity_classification_source'] ?? null) ? $metadata['legacy_identity_classification_source'] : null, 'legacy_identity_review_required' => (bool) ($metadata['legacy_identity_review_required'] ?? $this->migration_review_required), 'legacy_identity_result' => is_string($metadata['legacy_identity_result'] ?? null) ? $metadata['legacy_identity_result'] : null, 'legacy_identity_signals' => is_array($metadata['legacy_identity_signals'] ?? null) ? $metadata['legacy_identity_signals'] : [], 'legacy_identity_classified_at' => is_string($metadata['legacy_identity_classified_at'] ?? null) ? $metadata['legacy_identity_classified_at'] : null, 'effective_app' => [ 'app_id' => is_array($effectiveApp) && filled($effectiveApp['app_id'] ?? null) ? (string) $effectiveApp['app_id'] : null, 'source' => is_array($effectiveApp) && filled($effectiveApp['source'] ?? null) ? (string) $effectiveApp['source'] : $this->defaultEffectiveAppSource(), ], ]; } /** * @return array{app_id: ?string, source: string} */ public function effectiveAppMetadata(): array { $metadata = $this->legacyIdentityMetadata(); $effectiveApp = $metadata['effective_app']; if ($effectiveApp['app_id'] !== null || $effectiveApp['source'] === 'review_required') { return $effectiveApp; } if ($this->connection_type === ProviderConnectionType::Dedicated) { $payload = $this->credential?->payload; $clientId = is_array($payload) ? trim((string) ($payload['client_id'] ?? '')) : ''; return [ 'app_id' => $clientId !== '' ? $clientId : null, 'source' => 'dedicated_credential', ]; } $platformClientId = trim((string) config('graph.client_id')); return [ 'app_id' => $platformClientId !== '' ? $platformClientId : null, 'source' => 'platform_config', ]; } public function effectiveAppId(): ?string { return $this->effectiveAppMetadata()['app_id']; } public function requiresMigrationReview(): bool { return $this->legacyIdentityMetadata()['legacy_identity_review_required']; } /** * @return array */ public function classificationProjection( \App\Services\Providers\ProviderConnectionClassificationResult $result, \App\Services\Providers\ProviderConnectionStateProjector $stateProjector, ): array { $metadata = array_merge( is_array($this->metadata) ? $this->metadata : [], $result->metadata(), ['legacy_identity_classified_at' => now()->toJSON()], ); $projection = [ 'connection_type' => $result->suggestedConnectionType->value, 'migration_review_required' => $result->reviewRequired, 'metadata' => $metadata, ]; if ($result->reviewRequired) { $statusProjection = $stateProjector->project( connectionType: $result->suggestedConnectionType, consentStatus: $this->consent_status, verificationStatus: ProviderVerificationStatus::Blocked, currentStatus: is_string($this->status) ? $this->status : null, ); return $projection + [ 'verification_status' => ProviderVerificationStatus::Blocked->value, 'status' => $statusProjection['status'], 'health_status' => $statusProjection['health_status'], 'last_error_reason_code' => ProviderReasonCodes::ProviderConnectionReviewRequired, 'last_error_message' => 'Legacy provider connection requires explicit migration review.', 'migration_reviewed_at' => null, ]; } $currentVerificationStatus = $this->verification_status; $currentReasonCode = is_string($this->last_error_reason_code) ? $this->last_error_reason_code : null; if ( ($this->migration_review_required || $currentReasonCode === ProviderReasonCodes::ProviderConnectionReviewRequired) && $currentVerificationStatus === ProviderVerificationStatus::Blocked ) { $currentVerificationStatus = ProviderVerificationStatus::Unknown; } if ( $this->migration_review_required || $currentReasonCode === ProviderReasonCodes::ProviderConnectionReviewRequired ) { $statusProjection = $stateProjector->project( connectionType: $result->suggestedConnectionType, consentStatus: $this->consent_status, verificationStatus: $currentVerificationStatus, currentStatus: is_string($this->status) ? $this->status : null, ); return $projection + [ 'verification_status' => $currentVerificationStatus instanceof ProviderVerificationStatus ? $currentVerificationStatus->value : $currentVerificationStatus, 'status' => $statusProjection['status'], 'health_status' => $statusProjection['health_status'], 'last_error_reason_code' => $currentReasonCode === ProviderReasonCodes::ProviderConnectionReviewRequired ? null : $currentReasonCode, 'last_error_message' => $currentReasonCode === ProviderReasonCodes::ProviderConnectionReviewRequired ? null : $this->last_error_message, ]; } return $projection; } private function defaultEffectiveAppSource(): string { if ($this->connection_type === ProviderConnectionType::Dedicated) { return 'dedicated_credential'; } return 'platform_config'; } }