TenantAtlas/app/Jobs/GenerateEvidenceSnapshotJob.php
ahmido a74ab12f04 feat: implement evidence domain foundation (#183)
## 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
2026-03-19 13:32:52 +00:00

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;
}
}
}