Finding::factory()->for($tenant)->create([ 'status' => Finding::STATUS_RESOLVED, 'resolved_reason' => Finding::RESOLVE_REASON_REMEDIATED, ]), 'verified_cleared' => Finding::factory()->for($tenant)->create([ 'status' => Finding::STATUS_RESOLVED, 'resolved_reason' => Finding::RESOLVE_REASON_NO_LONGER_DRIFTING, ]), 'closed_duplicate' => Finding::factory()->for($tenant)->create([ 'status' => Finding::STATUS_CLOSED, 'closed_reason' => Finding::CLOSE_REASON_DUPLICATE, ]), 'risk_accepted' => Finding::factory()->for($tenant)->create([ 'status' => Finding::STATUS_RISK_ACCEPTED, 'closed_reason' => Finding::CLOSE_REASON_ACCEPTED_RISK, ]), ]; } function materializeFindingOutcomeSnapshot(\App\Models\Tenant $tenant): EvidenceSnapshot { $payload = app(EvidenceSnapshotService::class)->buildSnapshotPayload($tenant); $snapshot = EvidenceSnapshot::query()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'status' => EvidenceSnapshotStatus::Active->value, 'fingerprint' => $payload['fingerprint'], 'completeness_state' => $payload['completeness'], 'summary' => $payload['summary'], 'generated_at' => now(), ]); foreach ($payload['items'] as $item) { $snapshot->items()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'dimension_key' => $item['dimension_key'], 'state' => $item['state'], 'required' => $item['required'], 'source_kind' => $item['source_kind'], 'source_record_type' => $item['source_record_type'], 'source_record_id' => $item['source_record_id'], 'source_fingerprint' => $item['source_fingerprint'], 'measured_at' => $item['measured_at'], 'freshness_at' => $item['freshness_at'], 'summary_payload' => $item['summary_payload'], 'sort_order' => $item['sort_order'], ]); } return $snapshot->load('items'); } it('summarizes canonical terminal outcomes and report buckets from findings evidence', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); $findings = seedFindingOutcomeMatrix($tenant); $summary = app(FindingsSummarySource::class)->collect($tenant)['summary_payload'] ?? []; expect(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_RESOLVED_PENDING_VERIFICATION))->toBe(1) ->and(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED))->toBe(1) ->and(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_CLOSED_DUPLICATE))->toBe(1) ->and(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_RISK_ACCEPTED))->toBe(1) ->and(data_get($summary, 'report_bucket_counts.remediation_pending_verification'))->toBe(1) ->and(data_get($summary, 'report_bucket_counts.remediation_verified'))->toBe(1) ->and(data_get($summary, 'report_bucket_counts.administrative_closure'))->toBe(1) ->and(data_get($summary, 'report_bucket_counts.accepted_risk'))->toBe(1); $pendingEntry = collect($summary['entries'] ?? [])->firstWhere('id', (int) $findings['pending_verification']->getKey()); $verifiedEntry = collect($summary['entries'] ?? [])->firstWhere('id', (int) $findings['verified_cleared']->getKey()); expect(data_get($pendingEntry, 'terminal_outcome.key'))->toBe(FindingOutcomeSemantics::OUTCOME_RESOLVED_PENDING_VERIFICATION) ->and(data_get($pendingEntry, 'terminal_outcome.verification_state'))->toBe(FindingOutcomeSemantics::VERIFICATION_PENDING) ->and(data_get($verifiedEntry, 'terminal_outcome.key'))->toBe(FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED) ->and(data_get($verifiedEntry, 'terminal_outcome.verification_state'))->toBe(FindingOutcomeSemantics::VERIFICATION_VERIFIED); }); it('propagates finding outcome summaries into evidence snapshots tenant reviews and review packs', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); seedFindingOutcomeMatrix($tenant); $snapshot = materializeFindingOutcomeSnapshot($tenant); expect(data_get($snapshot->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_RESOLVED_PENDING_VERIFICATION))->toBe(1) ->and(data_get($snapshot->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED))->toBe(1) ->and(data_get($snapshot->summary, 'finding_report_buckets.accepted_risk'))->toBe(1); $review = composeTenantReviewForTest($tenant, $user, $snapshot); expect(data_get($review->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_CLOSED_DUPLICATE))->toBe(1) ->and(data_get($review->summary, 'finding_report_buckets.administrative_closure'))->toBe(1); setTenantPanelContext($tenant); $this->actingAs($user) ->get(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) ->assertOk() ->assertSee('Terminal outcomes:') ->assertSee('resolved pending verification') ->assertSee('verified cleared') ->assertSee('closed as duplicate') ->assertSee('risk accepted'); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); Livewire::actingAs($user) ->test(ReviewRegister::class) ->assertCanSeeTableRecords([$review]) ->assertSee('Terminal outcomes:') ->assertSee('resolved pending verification'); $pack = app(ReviewPackService::class)->generateFromReview($review, $user, [ 'include_pii' => false, 'include_operations' => false, ]); expect(data_get($pack->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_RISK_ACCEPTED))->toBe(1) ->and(data_get($pack->summary, 'finding_report_buckets.accepted_risk'))->toBe(1); });