validatePreconditions($profile, $sourceTenant); if ($precondition !== null) { return ['ok' => false, 'reason_code' => $precondition]; } try { $effectiveScope = BaselineScope::fromJsonb( is_array($profile->scope_jsonb) ? $profile->scope_jsonb : null, ); } catch (InvalidArgumentException) { return ['ok' => false, 'reason_code' => BaselineReasonCodes::CAPTURE_INVALID_SCOPE]; } if ($effectiveScope->allTypes() === []) { return ['ok' => false, 'reason_code' => BaselineReasonCodes::CAPTURE_INVALID_SCOPE]; } $eligibility = $effectiveScope->operationEligibility('capture', $this->capabilityGuard); if (! $eligibility['ok']) { return [ 'ok' => false, 'reason_code' => BaselineReasonCodes::CAPTURE_UNSUPPORTED_SCOPE, ]; } $truthfulTypes = $effectiveScope->toEffectiveScopeContext($this->capabilityGuard, 'capture')['truthful_types'] ?? null; $inventoryEligibility = $this->latestInventoryEligibilityDecision($sourceTenant, $effectiveScope, is_array($truthfulTypes) ? $truthfulTypes : null); if (! $inventoryEligibility['ok']) { return [ 'ok' => false, 'reason_code' => $inventoryEligibility['reason_code'], ]; } $captureMode = $profile->capture_mode instanceof BaselineCaptureMode ? $profile->capture_mode : BaselineCaptureMode::Opportunistic; $context = [ 'target_scope' => [ 'entra_tenant_id' => $sourceTenant->graphTenantId(), 'entra_tenant_name' => (string) $sourceTenant->name, ], 'baseline_profile_id' => (int) $profile->getKey(), 'source_tenant_id' => (int) $sourceTenant->getKey(), 'effective_scope' => $effectiveScope->toEffectiveScopeContext($this->capabilityGuard, 'capture'), 'capture_mode' => $captureMode->value, 'baseline_capture' => [ 'inventory_sync_run_id' => $inventoryEligibility['inventory_sync_run_id'], 'eligibility' => $this->eligibilityContextPayload($inventoryEligibility, phase: 'preflight'), ], ]; $run = $this->runs->ensureRunWithIdentity( tenant: $sourceTenant, type: OperationRunType::BaselineCapture->value, identityInputs: [ 'baseline_profile_id' => (int) $profile->getKey(), ], context: $context, initiator: $initiator, ); if ($run->wasRecentlyCreated) { CaptureBaselineSnapshotJob::dispatch($run); } return ['ok' => true, 'run' => $run]; } private function validatePreconditions(BaselineProfile $profile, Tenant $sourceTenant): ?string { if ($profile->status !== BaselineProfileStatus::Active) { return BaselineReasonCodes::CAPTURE_PROFILE_NOT_ACTIVE; } if ($profile->capture_mode === BaselineCaptureMode::FullContent && ! $this->rolloutGate->enabled()) { return BaselineReasonCodes::CAPTURE_ROLLOUT_DISABLED; } if ($sourceTenant->workspace_id === null) { return BaselineReasonCodes::CAPTURE_MISSING_SOURCE_TENANT; } if ((int) $sourceTenant->workspace_id !== (int) $profile->workspace_id) { return BaselineReasonCodes::CAPTURE_MISSING_SOURCE_TENANT; } return null; } /** * @param list|null $truthfulTypes * @return array{ * ok: bool, * reason_code: ?string, * inventory_sync_run_id: ?int, * inventory_outcome: ?string, * effective_types: list, * covered_types: list, * uncovered_types: list * } */ public function latestInventoryEligibilityDecision( Tenant $sourceTenant, BaselineScope $effectiveScope, ?array $truthfulTypes = null, ): array { $effectiveTypes = is_array($truthfulTypes) && $truthfulTypes !== [] ? array_values(array_unique(array_filter($truthfulTypes, 'is_string'))) : $effectiveScope->allTypes(); sort($effectiveTypes, SORT_STRING); $run = OperationRun::query() ->where('tenant_id', (int) $sourceTenant->getKey()) ->where('type', OperationRunType::InventorySync->value) ->where('status', OperationRunStatus::Completed->value) ->orderByDesc('completed_at') ->orderByDesc('id') ->first(); if (! $run instanceof OperationRun) { return [ 'ok' => false, 'reason_code' => BaselineReasonCodes::CAPTURE_INVENTORY_MISSING, 'inventory_sync_run_id' => null, 'inventory_outcome' => null, 'effective_types' => $effectiveTypes, 'covered_types' => [], 'uncovered_types' => $effectiveTypes, ]; } $outcome = is_string($run->outcome) ? trim($run->outcome) : null; if ($outcome === OperationRunOutcome::Blocked->value) { return [ 'ok' => false, 'reason_code' => BaselineReasonCodes::CAPTURE_INVENTORY_BLOCKED, 'inventory_sync_run_id' => (int) $run->getKey(), 'inventory_outcome' => $outcome, 'effective_types' => $effectiveTypes, 'covered_types' => [], 'uncovered_types' => $effectiveTypes, ]; } if ($outcome === OperationRunOutcome::Failed->value) { return [ 'ok' => false, 'reason_code' => BaselineReasonCodes::CAPTURE_INVENTORY_FAILED, 'inventory_sync_run_id' => (int) $run->getKey(), 'inventory_outcome' => $outcome, 'effective_types' => $effectiveTypes, 'covered_types' => [], 'uncovered_types' => $effectiveTypes, ]; } $coverage = InventoryCoverage::fromContext($run->context); $coveredTypes = $coverage instanceof InventoryCoverage ? array_values(array_intersect($effectiveTypes, $coverage->coveredTypes())) : []; sort($coveredTypes, SORT_STRING); $uncoveredTypes = array_values(array_diff($effectiveTypes, $coveredTypes)); sort($uncoveredTypes, SORT_STRING); if ($coveredTypes === []) { return [ 'ok' => false, 'reason_code' => BaselineReasonCodes::CAPTURE_UNUSABLE_COVERAGE, 'inventory_sync_run_id' => (int) $run->getKey(), 'inventory_outcome' => $outcome, 'effective_types' => $effectiveTypes, 'covered_types' => [], 'uncovered_types' => $effectiveTypes, ]; } return [ 'ok' => true, 'reason_code' => null, 'inventory_sync_run_id' => (int) $run->getKey(), 'inventory_outcome' => $outcome, 'effective_types' => $effectiveTypes, 'covered_types' => $coveredTypes, 'uncovered_types' => $uncoveredTypes, ]; } /** * @param array{ * ok: bool, * reason_code: ?string, * inventory_sync_run_id: ?int, * inventory_outcome: ?string, * effective_types: list, * covered_types: list, * uncovered_types: list * } $decision * @return array */ public function eligibilityContextPayload(array $decision, string $phase): array { return [ 'phase' => $phase, 'ok' => (bool) ($decision['ok'] ?? false), 'reason_code' => is_string($decision['reason_code'] ?? null) ? $decision['reason_code'] : null, 'inventory_sync_run_id' => is_numeric($decision['inventory_sync_run_id'] ?? null) ? (int) $decision['inventory_sync_run_id'] : null, 'inventory_outcome' => is_string($decision['inventory_outcome'] ?? null) ? $decision['inventory_outcome'] : null, 'effective_types' => array_values(array_filter((array) ($decision['effective_types'] ?? []), 'is_string')), 'covered_types' => array_values(array_filter((array) ($decision['covered_types'] ?? []), 'is_string')), 'uncovered_types' => array_values(array_filter((array) ($decision['uncovered_types'] ?? []), 'is_string')), ]; } }