TenantAtlas/app/Services/TenantReviews/TenantReviewFingerprint.php
ahmido a4f2629493 feat: add tenant review layer (#185)
## Summary
- add the tenant review domain with tenant-scoped review library, canonical workspace review register, lifecycle actions, and review-derived executive pack export
- extend review pack, operations, audit, capability, and badge infrastructure to support review composition, publication, export, and recurring review cycles
- add product backlog and audit documentation updates for tenant review and semantic-clarity follow-up candidates

## Testing
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact --filter="TenantReview"`
- `CI=1 vendor/bin/sail artisan test --compact`

## Notes
- Livewire v4+ compliant via existing Filament v5 stack
- panel providers remain in `bootstrap/providers.php` via existing Laravel 12 structure; no provider registration moved to `bootstrap/app.php`
- `TenantReviewResource` is not globally searchable, so the Filament edit/view global-search constraint does not apply
- destructive review actions use action handlers with confirmation and policy enforcement

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #185
2026-03-21 22:03:01 +00:00

42 lines
1.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantReviews;
use App\Models\EvidenceSnapshot;
use App\Models\Tenant;
use Illuminate\Support\Arr;
final class TenantReviewFingerprint
{
public function forSnapshot(Tenant $tenant, EvidenceSnapshot $snapshot): string
{
$summary = is_array($snapshot->summary) ? $snapshot->summary : [];
$payload = [
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'snapshot_fingerprint' => (string) $snapshot->fingerprint,
'snapshot_completeness' => (string) $snapshot->completeness_state,
'dimension_states' => collect(Arr::wrap($summary['dimensions'] ?? []))
->map(static fn (mixed $dimension): array => [
'key' => (string) data_get($dimension, 'key'),
'state' => (string) data_get($dimension, 'state'),
'required' => (bool) data_get($dimension, 'required', false),
])
->sortBy('key')
->values()
->all(),
'counts' => [
'finding_count' => (int) ($summary['finding_count'] ?? 0),
'report_count' => (int) ($summary['report_count'] ?? 0),
'operation_count' => (int) ($summary['operation_count'] ?? 0),
],
];
return hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR));
}
}