AuditActorType::class, 'metadata' => 'array', 'outcome' => AuditOutcome::class, 'recorded_at' => 'datetime', ]; protected static function booted(): void { static::creating(function (self $auditLog): void { if ($auditLog->workspace_id === null && is_numeric($auditLog->tenant_id)) { $workspaceId = Tenant::query() ->whereKey((int) $auditLog->tenant_id) ->value('workspace_id'); if (is_numeric($workspaceId)) { $auditLog->workspace_id = (int) $workspaceId; } } $derived = app(AuditEventBuilder::class)->fillMissingDerivedAttributes($auditLog->getAttributes()); $auditLog->forceFill([ 'workspace_id' => $derived['workspace_id'] ?? $auditLog->workspace_id, 'actor_type' => $derived['actor_type'] ?? $auditLog->actor_type, 'actor_label' => $derived['actor_label'] ?? $auditLog->actor_label, 'resource_type' => $derived['resource_type'] ?? $auditLog->resource_type, 'resource_id' => $derived['resource_id'] ?? $auditLog->resource_id, 'target_label' => $derived['target_label'] ?? $auditLog->target_label, 'status' => $derived['status'] ?? $auditLog->status, 'outcome' => $derived['outcome'] ?? $auditLog->outcome, 'summary' => $derived['summary'] ?? $auditLog->summary, 'metadata' => $derived['metadata'] ?? $auditLog->metadata, 'operation_run_id' => $derived['operation_run_id'] ?? $auditLog->operation_run_id, 'recorded_at' => $derived['recorded_at'] ?? $auditLog->recorded_at, ]); }); } public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class); } public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } public function operationRun(): BelongsTo { return $this->belongsTo(OperationRun::class); } public function scopeForWorkspace(Builder $query, int $workspaceId): Builder { return $query->where('workspace_id', $workspaceId); } public function scopeForTenant(Builder $query, int $tenantId): Builder { return $query->where('tenant_id', $tenantId); } public function scopeLatestFirst(Builder $query): Builder { return $query->orderByDesc('recorded_at')->orderByDesc('id'); } public function summaryText(): string { if (filled($this->summary)) { return $this->formattedSummary((string) $this->summary); } return AuditActionId::summaryFor( action: (string) $this->action, targetLabel: $this->targetDisplayLabel(), targetType: is_string($this->resource_type) ? $this->resource_type : null, context: is_array($this->metadata) ? $this->metadata : [], ); } public function normalizedOutcome(): AuditOutcome { return $this->outcome instanceof AuditOutcome ? $this->outcome : AuditOutcome::normalize($this->outcome ?? $this->status); } public function actorSnapshot(): AuditActorSnapshot { $type = $this->actor_type instanceof AuditActorType ? $this->actor_type : (is_string($this->actor_type) && trim($this->actor_type) !== '' ? AuditActorType::normalize($this->actor_type) : AuditActorType::infer( action: is_string($this->action) ? $this->action : null, actorId: is_numeric($this->actor_id) ? (int) $this->actor_id : null, actorEmail: is_string($this->actor_email) ? $this->actor_email : null, actorName: is_string($this->actor_name ?? null) ? (string) $this->actor_name : null, context: is_array($this->metadata) ? $this->metadata : [], )); return AuditActorSnapshot::fromLegacy( type: $type, id: is_numeric($this->actor_id) ? (int) $this->actor_id : null, email: is_string($this->actor_email) ? $this->actor_email : null, label: is_string($this->actor_label ?? $this->actor_name ?? null) ? (string) ($this->actor_label ?? $this->actor_name) : null, ); } public function targetSnapshot(): AuditTargetSnapshot { $type = is_string($this->resource_type) ? $this->resource_type : null; $id = is_string($this->resource_id) || is_numeric($this->resource_id) ? (string) $this->resource_id : null; $label = is_string($this->target_label) ? $this->target_label : null; if ($type === 'workspace_setting') { $label = $this->formattedWorkspaceSettingLabel($label ?? $id); } return new AuditTargetSnapshot( type: $type, id: $id, label: $label, ); } public function actorDisplayLabel(): string { return $this->actorSnapshot()->labelOrFallback(); } public function targetDisplayLabel(): ?string { return $this->targetSnapshot()->labelOrFallback(); } /** * @return list */ public function contextItems(): array { $metadata = is_array($this->metadata) ? $this->metadata : []; $items = []; $seen = []; foreach ([ 'reason', 'reason_code', 'before_status', 'after_status', 'from_role', 'to_role', 'item_count', 'policy_count', 'assignment_count', 'status', 'source', 'attempted_action', ] as $key) { $value = $metadata[$key] ?? null; if (! is_scalar($value) || $value === '') { continue; } $items[] = [ 'label' => ucfirst(str_replace('_', ' ', $key)), 'value' => $value, ]; $seen[] = $key; } foreach ($metadata as $key => $value) { if (in_array($key, $seen, true) || in_array($key, ['before', 'after'], true)) { continue; } if (! is_scalar($value) || $value === '') { continue; } $items[] = [ 'label' => ucfirst(str_replace('_', ' ', (string) $key)), 'value' => $value, ]; if (count($items) >= 10) { break; } } return $items; } /** * @return array */ public function technicalMetadata(): array { return array_filter([ 'Event type' => $this->action, 'Legacy status' => $this->status, 'Outcome' => $this->normalizedOutcome()->value, 'Actor id' => is_numeric($this->actor_id) ? (int) $this->actor_id : null, 'Target type' => $this->resource_type, 'Target id' => $this->resource_id, 'Operation run' => is_numeric($this->operation_run_id) ? (int) $this->operation_run_id : null, 'Tenant scope' => is_numeric($this->tenant_id) ? (int) $this->tenant_id : null, 'Workspace scope' => is_numeric($this->workspace_id) ? (int) $this->workspace_id : null, ], static fn (mixed $value): bool => $value !== null && $value !== ''); } private function formattedSummary(string $summary): string { if ($this->resource_type !== 'workspace_setting') { return $summary; } $rawTarget = $this->rawWorkspaceSettingTarget(); $formattedTarget = $this->formattedWorkspaceSettingLabel($rawTarget); if ($rawTarget === null || $formattedTarget === null || $rawTarget === $formattedTarget) { return $summary; } return str_replace($rawTarget, $formattedTarget, $summary); } private function rawWorkspaceSettingTarget(): ?string { if ($this->resource_type !== 'workspace_setting') { return null; } if (is_string($this->target_label) && trim($this->target_label) !== '') { return trim($this->target_label); } if (is_string($this->resource_id) && trim($this->resource_id) !== '') { return trim($this->resource_id); } return null; } private function formattedWorkspaceSettingLabel(?string $value): ?string { if (! is_string($value) || trim($value) === '') { return null; } return Str::of(str_replace('.', ' ', trim($value)))->headline()->toString(); } }