operationRun = $operationRun; } public function middleware(): array { return [new TrackOperationRun]; } public function handle(EntraGroupSyncService $syncService, AuditLogger $auditLogger): void { if (! $this->operationRun) { $this->fail(new RuntimeException('OperationRun context is required for EntraGroupSyncJob.')); return; } $tenant = Tenant::query()->find($this->tenantId); if (! $tenant instanceof Tenant) { throw new RuntimeException('Tenant not found.'); } $legacyRun = $this->resolveLegacyRun($tenant); if ($legacyRun instanceof EntraGroupSyncRun) { if ($legacyRun->status !== EntraGroupSyncRun::STATUS_PENDING) { return; } $legacyRun->update([ 'status' => EntraGroupSyncRun::STATUS_RUNNING, 'started_at' => CarbonImmutable::now('UTC'), ]); $auditLogger->log( tenant: $tenant, action: 'directory_groups.sync.started', context: [ 'selection_key' => $legacyRun->selection_key, 'run_id' => $legacyRun->getKey(), 'slot_key' => $legacyRun->slot_key, ], actorId: $legacyRun->initiator_user_id, status: 'success', resourceType: 'entra_group_sync_run', resourceId: (string) $legacyRun->getKey(), ); } else { $auditLogger->log( tenant: $tenant, action: 'directory_groups.sync.started', context: [ 'selection_key' => $this->selectionKey, 'slot_key' => $this->slotKey, ], actorId: $this->operationRun->user_id, status: 'success', resourceType: 'operation_run', resourceId: (string) $this->operationRun->getKey(), ); } $result = $syncService->sync($tenant, $this->selectionKey); $terminalStatus = EntraGroupSyncRun::STATUS_SUCCEEDED; if ($result['error_code'] !== null) { $terminalStatus = EntraGroupSyncRun::STATUS_FAILED; } elseif ($result['safety_stop_triggered'] === true) { $terminalStatus = EntraGroupSyncRun::STATUS_PARTIAL; } if ($legacyRun instanceof EntraGroupSyncRun) { $legacyRun->update([ 'status' => $terminalStatus, 'pages_fetched' => $result['pages_fetched'], 'items_observed_count' => $result['items_observed_count'], 'items_upserted_count' => $result['items_upserted_count'], 'error_count' => $result['error_count'], 'safety_stop_triggered' => $result['safety_stop_triggered'], 'safety_stop_reason' => $result['safety_stop_reason'], 'error_code' => $result['error_code'], 'error_category' => $result['error_category'], 'error_summary' => $result['error_summary'], 'finished_at' => CarbonImmutable::now('UTC'), ]); } /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opOutcome = match ($terminalStatus) { EntraGroupSyncRun::STATUS_SUCCEEDED => 'succeeded', EntraGroupSyncRun::STATUS_PARTIAL => 'partially_succeeded', EntraGroupSyncRun::STATUS_FAILED => 'failed', default => 'failed', }; $failures = []; if (is_string($result['error_code']) && $result['error_code'] !== '') { $failures[] = [ 'code' => $result['error_code'], 'message' => is_string($result['error_summary']) ? $result['error_summary'] : 'Directory groups sync failed.', ]; } $opService->updateRun( $this->operationRun, 'completed', $opOutcome, [ // NOTE: summary_counts are normalized to a fixed whitelist for Ops UX. // Keep keys aligned with App\Support\OpsUx\OperationSummaryKeys. 'total' => $result['items_observed_count'], 'processed' => $result['items_observed_count'], 'updated' => $result['items_upserted_count'], 'failed' => $result['error_count'], ], $failures, ); if ($legacyRun instanceof EntraGroupSyncRun) { $auditLogger->log( tenant: $tenant, action: $terminalStatus === EntraGroupSyncRun::STATUS_SUCCEEDED ? 'directory_groups.sync.succeeded' : ($terminalStatus === EntraGroupSyncRun::STATUS_PARTIAL ? 'directory_groups.sync.partial' : 'directory_groups.sync.failed'), context: [ 'selection_key' => $legacyRun->selection_key, 'run_id' => $legacyRun->getKey(), 'slot_key' => $legacyRun->slot_key, 'pages_fetched' => $legacyRun->pages_fetched, 'items_observed_count' => $legacyRun->items_observed_count, 'items_upserted_count' => $legacyRun->items_upserted_count, 'error_code' => $legacyRun->error_code, 'error_category' => $legacyRun->error_category, ], actorId: $legacyRun->initiator_user_id, status: $terminalStatus === EntraGroupSyncRun::STATUS_FAILED ? 'failed' : 'success', resourceType: 'entra_group_sync_run', resourceId: (string) $legacyRun->getKey(), ); return; } $auditLogger->log( tenant: $tenant, action: $terminalStatus === EntraGroupSyncRun::STATUS_SUCCEEDED ? 'directory_groups.sync.succeeded' : ($terminalStatus === EntraGroupSyncRun::STATUS_PARTIAL ? 'directory_groups.sync.partial' : 'directory_groups.sync.failed'), context: [ 'selection_key' => $this->selectionKey, 'slot_key' => $this->slotKey, 'pages_fetched' => $result['pages_fetched'], 'items_observed_count' => $result['items_observed_count'], 'items_upserted_count' => $result['items_upserted_count'], 'error_code' => $result['error_code'], 'error_category' => $result['error_category'], ], actorId: $this->operationRun->user_id, status: $terminalStatus === EntraGroupSyncRun::STATUS_FAILED ? 'failed' : 'success', resourceType: 'operation_run', resourceId: (string) $this->operationRun->getKey(), ); } private function resolveLegacyRun(Tenant $tenant): ?EntraGroupSyncRun { if ($this->runId !== null) { $run = EntraGroupSyncRun::query() ->whereKey($this->runId) ->where('tenant_id', $tenant->getKey()) ->first(); if ($run instanceof EntraGroupSyncRun) { return $run; } return null; } if ($this->slotKey !== null) { $run = EntraGroupSyncRun::query() ->where('tenant_id', $tenant->getKey()) ->where('selection_key', $this->selectionKey) ->where('slot_key', $this->slotKey) ->first(); if ($run instanceof EntraGroupSyncRun) { return $run; } return null; } return null; } }