create(); $draft = createOnboardingDraft([ 'workspace' => $tenant->workspace, 'tenant' => $tenant, 'current_step' => 'verify', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, 'provider_connection_id' => 42, 'connection_recently_updated' => true, ], ]); $snapshot = app(OnboardingLifecycleService::class)->snapshot($draft); expect($snapshot['lifecycle_state'])->toBe(OnboardingLifecycleState::ActionRequired) ->and($snapshot['current_checkpoint'])->toBe(OnboardingCheckpoint::VerifyAccess) ->and($snapshot['last_completed_checkpoint'])->toBe(OnboardingCheckpoint::ConnectProvider) ->and($snapshot['reason_code'])->toBe('provider_connection_changed') ->and($snapshot['blocking_reason_code'])->toBe('provider_connection_changed'); }); it('marks a draft as ready for activation when verification succeeded for the selected connection', function (): void { $tenant = Tenant::factory()->create(); $draft = createOnboardingDraft([ 'workspace' => $tenant->workspace, 'tenant' => $tenant, 'current_step' => 'verify', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, 'provider_connection_id' => 84, ], ]); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, 'context' => [ 'provider_connection_id' => 84, ], ]); $draft->forceFill([ 'state' => array_merge($draft->state ?? [], [ 'verification_operation_run_id' => (int) $run->getKey(), ]), ])->save(); $service = app(OnboardingLifecycleService::class); $snapshot = $service->snapshot($draft->fresh()); expect($snapshot['lifecycle_state'])->toBe(OnboardingLifecycleState::ReadyForActivation) ->and($snapshot['current_checkpoint'])->toBe(OnboardingCheckpoint::CompleteActivate) ->and($snapshot['last_completed_checkpoint'])->toBe(OnboardingCheckpoint::VerifyAccess) ->and($service->isReadyForActivation($draft->fresh()))->toBeTrue(); }); it('marks a draft as bootstrapping while a selected bootstrap run is still active', function (): void { $tenant = Tenant::factory()->create(); $verificationRun = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, 'context' => [ 'provider_connection_id' => 126, ], ]); $bootstrapRun = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'status' => OperationRunStatus::Running->value, 'outcome' => OperationRunOutcome::Pending->value, 'type' => 'inventory_sync', 'context' => [ 'provider_connection_id' => 126, ], ]); $draft = createOnboardingDraft([ 'workspace' => $tenant->workspace, 'tenant' => $tenant, 'current_step' => 'bootstrap', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, 'provider_connection_id' => 126, 'verification_operation_run_id' => (int) $verificationRun->getKey(), 'bootstrap_operation_types' => ['inventory_sync'], 'bootstrap_operation_runs' => [ 'inventory_sync' => (int) $bootstrapRun->getKey(), ], ], ]); $service = app(OnboardingLifecycleService::class); $snapshot = $service->snapshot($draft); expect($snapshot['lifecycle_state'])->toBe(OnboardingLifecycleState::Bootstrapping) ->and($snapshot['current_checkpoint'])->toBe(OnboardingCheckpoint::Bootstrap) ->and($snapshot['last_completed_checkpoint'])->toBe(OnboardingCheckpoint::VerifyAccess) ->and($service->hasActiveCheckpoint($draft))->toBeTrue(); }); it('marks a draft as action required when a selected bootstrap run fails', function (): void { $tenant = Tenant::factory()->create(); $verificationRun = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, 'context' => [ 'provider_connection_id' => 256, ], ]); $bootstrapRun = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'type' => 'inventory_sync', 'context' => [ 'provider_connection_id' => 256, ], ]); $draft = createOnboardingDraft([ 'workspace' => $tenant->workspace, 'tenant' => $tenant, 'current_step' => 'bootstrap', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, 'provider_connection_id' => 256, 'verification_operation_run_id' => (int) $verificationRun->getKey(), 'bootstrap_operation_types' => ['inventory_sync'], 'bootstrap_operation_runs' => [ 'inventory_sync' => (int) $bootstrapRun->getKey(), ], ], ]); $snapshot = app(OnboardingLifecycleService::class)->snapshot($draft); expect($snapshot['lifecycle_state'])->toBe(OnboardingLifecycleState::ActionRequired) ->and($snapshot['current_checkpoint'])->toBe(OnboardingCheckpoint::Bootstrap) ->and($snapshot['last_completed_checkpoint'])->toBe(OnboardingCheckpoint::VerifyAccess) ->and($snapshot['reason_code'])->toBe('bootstrap_failed') ->and($snapshot['blocking_reason_code'])->toBe('bootstrap_failed'); }); it('applies the canonical lifecycle fields and normalizes the version floor', function (): void { $tenant = Tenant::factory()->create(); $draft = createOnboardingDraft([ 'workspace' => $tenant->workspace, 'tenant' => $tenant, 'version' => 0, 'lifecycle_state' => OnboardingLifecycleState::Draft->value, 'current_checkpoint' => OnboardingCheckpoint::Identify->value, 'last_completed_checkpoint' => null, 'current_step' => 'verify', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, 'provider_connection_id' => 999, 'connection_recently_updated' => true, ], ]); $changed = app(OnboardingLifecycleService::class)->applySnapshot($draft, false); expect($changed)->toBeTrue() ->and($draft->version)->toBe(1) ->and($draft->lifecycle_state)->toBe(OnboardingLifecycleState::ActionRequired) ->and($draft->current_checkpoint)->toBe(OnboardingCheckpoint::VerifyAccess) ->and($draft->reason_code)->toBe('provider_connection_changed'); });