operationRun = $operationRun; } /** * Get the middleware the job should pass through. * * @return array */ public function middleware(): array { return [new TrackOperationRun]; } /** * Execute the job. */ public function handle(InventorySyncService $inventorySyncService, AuditLogger $auditLogger, OperationRunService $operationRunService): void { if (! $this->operationRun) { $this->fail(new RuntimeException('OperationRun context is required for RunInventorySyncJob.')); return; } $tenant = Tenant::query()->find($this->tenantId); if (! $tenant instanceof Tenant) { throw new RuntimeException('Tenant not found.'); } $user = User::query()->find($this->userId); if (! $user instanceof User) { throw new RuntimeException('User not found.'); } $context = is_array($this->operationRun->context) ? $this->operationRun->context : []; $policyTypes = $context['policy_types'] ?? []; $policyTypes = is_array($policyTypes) ? array_values(array_filter(array_map('strval', $policyTypes))) : []; $processedPolicyTypes = []; $successCount = 0; $failedCount = 0; // Note: The TrackOperationRun middleware will automatically set status to 'running' at start. // It will also handle success completion if no exceptions thrown. // However, InventorySyncService execution logic might be complex with partial failures. // We might want to explicitly update the OperationRun if partial failures occur. $result = $inventorySyncService->executeSelection( $this->operationRun, $tenant, $context, function (string $policyType, bool $success, ?string $errorCode) use (&$processedPolicyTypes, &$successCount, &$failedCount): void { $processedPolicyTypes[] = $policyType; if ($success) { $successCount++; return; } $failedCount++; }, ); $status = (string) ($result['status'] ?? 'failed'); $errorCodes = is_array($result['error_codes'] ?? null) ? $result['error_codes'] : []; $reason = (string) ($errorCodes[0] ?? $status); $itemsObserved = (int) ($result['items_observed_count'] ?? 0); $itemsUpserted = (int) ($result['items_upserted_count'] ?? 0); $errorsCount = (int) ($result['errors_count'] ?? 0); if ($status === 'success') { $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Succeeded->value, summaryCounts: [ 'total' => count($policyTypes), 'processed' => count($policyTypes), 'succeeded' => count($policyTypes), 'failed' => 0, 'items' => $itemsObserved, 'updated' => $itemsUpserted, ], ); $auditLogger->log( tenant: $tenant, action: 'inventory.sync.completed', context: [ 'metadata' => [ 'operation_run_id' => (int) $this->operationRun->getKey(), 'observed' => $itemsObserved, 'upserted' => $itemsUpserted, ], ], actorId: $user->id, actorEmail: $user->email, actorName: $user->name, resourceType: 'operation_run', resourceId: (string) $this->operationRun->getKey(), ); return; } if ($status === 'partial') { $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::PartiallySucceeded->value, summaryCounts: [ 'total' => count($policyTypes), 'processed' => count($policyTypes), 'succeeded' => max(0, count($policyTypes) - $errorsCount), 'failed' => $errorsCount, 'items' => $itemsObserved, 'updated' => $itemsUpserted, ], failures: [ ['code' => 'inventory.partial', 'message' => "Errors: {$errorsCount}"], ], ); $auditLogger->log( tenant: $tenant, action: 'inventory.sync.partial', context: [ 'metadata' => [ 'operation_run_id' => (int) $this->operationRun->getKey(), 'observed' => $itemsObserved, 'upserted' => $itemsUpserted, 'errors' => $errorsCount, ], ], actorId: $user->id, actorEmail: $user->email, actorName: $user->name, status: 'failure', resourceType: 'operation_run', resourceId: (string) $this->operationRun->getKey(), ); return; } if ($status === 'skipped') { $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Failed->value, summaryCounts: [ 'total' => count($policyTypes), 'processed' => count($policyTypes), 'succeeded' => 0, 'failed' => 0, 'skipped' => count($policyTypes), ], failures: [ ['code' => 'inventory.skipped', 'message' => $reason], ], ); $auditLogger->log( tenant: $tenant, action: 'inventory.sync.skipped', context: [ 'metadata' => [ 'operation_run_id' => (int) $this->operationRun->getKey(), 'reason' => $reason, ], ], actorId: $user->id, actorEmail: $user->email, actorName: $user->name, resourceType: 'operation_run', resourceId: (string) $this->operationRun->getKey(), ); return; } $missingPolicyTypes = array_values(array_diff($policyTypes, array_unique($processedPolicyTypes))); $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Failed->value, summaryCounts: [ 'total' => count($policyTypes), 'processed' => count($policyTypes), 'succeeded' => $successCount, 'failed' => max($failedCount, count($missingPolicyTypes)), ], failures: [ ['code' => 'inventory.failed', 'message' => $reason], ], ); $auditLogger->log( tenant: $tenant, action: 'inventory.sync.failed', context: [ 'metadata' => [ 'operation_run_id' => (int) $this->operationRun->getKey(), 'reason' => $reason, ], ], actorId: $user->id, actorEmail: $user->email, actorName: $user->name, status: 'failure', resourceType: 'operation_run', resourceId: (string) $this->operationRun->getKey(), ); } }