## Summary - add the Evidence Snapshot domain with immutable tenant-scoped snapshots, per-dimension items, queued generation, audit actions, badge mappings, and Filament list/detail surfaces - add the workspace evidence overview, capability and policy wiring, Livewire update-path hardening, and review-pack integration through explicit evidence snapshot resolution - add spec 153 artifacts, migrations, factories, and focused Pest coverage for evidence, review-pack reuse, authorization, action-surface regressions, and audit behavior ## Testing - `vendor/bin/sail artisan test --compact --stop-on-failure` - `CI=1 vendor/bin/sail artisan test --compact` - `vendor/bin/sail bin pint --dirty --format agent` ## Notes - branch: `153-evidence-domain-foundation` - commit: `b7dfa279` - spec: `specs/153-evidence-domain-foundation/` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #183
119 lines
4.6 KiB
PHP
119 lines
4.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\OperationRun;
|
|
use App\Services\Evidence\EvidenceSnapshotService;
|
|
use App\Services\OperationRunService;
|
|
use App\Support\Evidence\EvidenceSnapshotStatus;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Queue\Queueable;
|
|
use Throwable;
|
|
|
|
class GenerateEvidenceSnapshotJob implements ShouldQueue
|
|
{
|
|
use Queueable;
|
|
|
|
public function __construct(
|
|
public int $snapshotId,
|
|
public int $operationRunId,
|
|
) {}
|
|
|
|
public function handle(EvidenceSnapshotService $service, OperationRunService $operationRuns): void
|
|
{
|
|
$snapshot = EvidenceSnapshot::query()->with('tenant')->find($this->snapshotId);
|
|
$operationRun = OperationRun::query()->find($this->operationRunId);
|
|
|
|
if (! $snapshot instanceof EvidenceSnapshot || ! $operationRun instanceof OperationRun || ! $snapshot->tenant) {
|
|
return;
|
|
}
|
|
|
|
$operationRuns->updateRun($operationRun, OperationRunStatus::Running->value, OperationRunOutcome::Pending->value);
|
|
$snapshot->update(['status' => EvidenceSnapshotStatus::Generating->value]);
|
|
|
|
try {
|
|
$payload = $service->buildSnapshotPayload($snapshot->tenant);
|
|
$previousActive = EvidenceSnapshot::query()
|
|
->where('tenant_id', (int) $snapshot->tenant_id)
|
|
->where('workspace_id', (int) $snapshot->workspace_id)
|
|
->where('status', EvidenceSnapshotStatus::Active->value)
|
|
->whereKeyNot((int) $snapshot->getKey())
|
|
->first();
|
|
|
|
$snapshot->items()->delete();
|
|
|
|
foreach ($payload['items'] as $item) {
|
|
$snapshot->items()->create([
|
|
'tenant_id' => (int) $snapshot->tenant_id,
|
|
'workspace_id' => (int) $snapshot->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'],
|
|
]);
|
|
}
|
|
|
|
if ($previousActive instanceof EvidenceSnapshot && $previousActive->fingerprint !== $payload['fingerprint']) {
|
|
$previousActive->update([
|
|
'status' => EvidenceSnapshotStatus::Superseded->value,
|
|
]);
|
|
}
|
|
|
|
$snapshot->update([
|
|
'fingerprint' => $payload['fingerprint'],
|
|
'previous_fingerprint' => $previousActive?->fingerprint,
|
|
'status' => EvidenceSnapshotStatus::Active->value,
|
|
'completeness_state' => $payload['completeness'],
|
|
'generated_at' => now(),
|
|
'summary' => $payload['summary'],
|
|
]);
|
|
|
|
$operationRuns->updateRun(
|
|
$operationRun,
|
|
status: OperationRunStatus::Completed->value,
|
|
outcome: OperationRunOutcome::Succeeded->value,
|
|
summaryCounts: [
|
|
'created' => 1,
|
|
'finding_count' => (int) ($payload['summary']['finding_count'] ?? 0),
|
|
'report_count' => (int) ($payload['summary']['report_count'] ?? 0),
|
|
'operation_count' => (int) ($payload['summary']['operation_count'] ?? 0),
|
|
'errors_recorded' => 0,
|
|
],
|
|
);
|
|
} catch (Throwable $throwable) {
|
|
$snapshot->update([
|
|
'status' => EvidenceSnapshotStatus::Failed->value,
|
|
'summary' => [
|
|
'error' => $throwable->getMessage(),
|
|
],
|
|
]);
|
|
|
|
$operationRuns->updateRun(
|
|
$operationRun,
|
|
status: OperationRunStatus::Completed->value,
|
|
outcome: OperationRunOutcome::Failed->value,
|
|
failures: [
|
|
[
|
|
'code' => 'evidence_snapshot_generation.failed',
|
|
'message' => $throwable->getMessage(),
|
|
],
|
|
],
|
|
);
|
|
|
|
throw $throwable;
|
|
}
|
|
}
|
|
}
|