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++; }, ); $updatedContext = is_array($this->operationRun->context) ? $this->operationRun->context : []; $updatedContext['result'] = [ 'had_errors' => (bool) ($result['had_errors'] ?? true), 'error_codes' => is_array($result['error_codes'] ?? null) ? array_values($result['error_codes']) : [], 'error_context' => is_array($result['error_context'] ?? null) ? $result['error_context'] : null, ]; $this->operationRun->update([ 'context' => $updatedContext, ]); $this->operationRun->refresh(); $status = (string) ($result['status'] ?? 'failed'); $errorCodes = is_array($result['error_codes'] ?? null) ? $result['error_codes'] : []; $reason = (string) ($errorCodes[0] ?? $status); $errorContext = is_array($result['error_context'] ?? null) ? $result['error_context'] : []; $sanitizedErrorMessage = is_string($errorContext['message'] ?? null) ? (string) $errorContext['message'] : null; $reasonCode = null; $sanitizedMessageWithoutReasonCode = null; if (is_string($sanitizedErrorMessage)) { $sanitizedMessageWithoutReasonCode = preg_replace('/^\[[^\]]+\]\s*/', '', $sanitizedErrorMessage); $sanitizedMessageWithoutReasonCode = is_string($sanitizedMessageWithoutReasonCode) ? trim($sanitizedMessageWithoutReasonCode) : null; if (preg_match('/^\[(?[^\]]+)\]/', $sanitizedErrorMessage, $m)) { $candidate = (string) ($m['code'] ?? ''); if ($candidate !== '' && ProviderReasonCodes::isKnown($candidate)) { $reasonCode = $candidate; } } } if ($reason === 'unexpected_exception' && is_string($sanitizedErrorMessage) && $sanitizedErrorMessage !== '') { $reason = is_string($sanitizedMessageWithoutReasonCode) && $sanitizedMessageWithoutReasonCode !== '' ? $sanitizedMessageWithoutReasonCode : $sanitizedErrorMessage; } $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', 'reason_code' => $reasonCode, '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(), ); } }