value]; } public function supportsType(string $type): bool { return OperationCatalog::canonicalCode($type) === OperationRunType::BackupScheduleExecute->value; } public function reconcile(OperationRun $run): ?ReconciliationResult { if (! $this->supportsType((string) $run->type)) { return ReconciliationResult::unsupported( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'This adapter only supports backup schedule execution runs.', evidence: [ 'adapter' => $this->key(), 'type' => (string) $run->type, ], ); } $context = is_array($run->context) ? $run->context : []; $scheduleId = is_numeric($context['backup_schedule_id'] ?? null) ? (int) $context['backup_schedule_id'] : null; $backupSetId = is_numeric($context['backup_set_id'] ?? null) ? (int) $context['backup_set_id'] : null; $evidence = [ 'adapter' => $this->key(), 'operation_run_id' => (int) $run->getKey(), 'workspace_id' => (int) $run->workspace_id, 'managed_environment_id' => (int) $run->managed_environment_id, 'backup_schedule_id' => $scheduleId, 'backup_set_id' => $backupSetId, ]; [$backupSet, $scopeProblem] = $this->resolveBackupSet($run, $backupSetId); if ($scopeProblem !== null) { return ReconciliationResult::notReconciled( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: $scopeProblem, evidence: $evidence, ); } if ($backupSet instanceof BackupSet) { $failures = is_array($backupSet->metadata['failures'] ?? null) ? $backupSet->metadata['failures'] : []; $failureCount = count($failures); $itemCount = max(0, (int) ($backupSet->item_count ?? 0)); $summaryCounts = [ 'total' => $itemCount + $failureCount, 'processed' => $itemCount + $failureCount, 'succeeded' => $itemCount, 'failed' => $failureCount, 'created' => 1, 'updated' => $itemCount, 'items' => $itemCount + $failureCount, ]; $related = [ 'type' => 'backup_set', 'id' => (int) $backupSet->getKey(), 'status' => (string) $backupSet->status, 'item_count' => $itemCount, ]; if ((string) $backupSet->status === 'completed' && $failureCount === 0) { return ReconciliationResult::reconciledSucceeded( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The backup schedule already produced a complete backup set before the run finished updating.', evidence: $evidence + ['chosen_backup_set_id' => (int) $backupSet->getKey()], related: $related, summaryCounts: $summaryCounts, ); } if ($itemCount > 0 && ((string) $backupSet->status === 'partial' || $failureCount > 0)) { return new ReconciliationResult( decision: 'reconciled_partially_succeeded', status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::PartiallySucceeded->value, reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The backup schedule produced a usable backup set, but some captures did not finish cleanly.', evidence: $evidence + ['chosen_backup_set_id' => (int) $backupSet->getKey()], related: $related, summaryCounts: $summaryCounts, failures: [[ 'code' => 'backup_schedule.partial', 'reason_code' => LifecycleReconciliationReason::AdapterOutOfSync->value, 'message' => 'The backup schedule produced a usable backup set, but some captures did not finish cleanly.', ]], safeForAutoCompletion: true, ); } if ((string) $backupSet->status === 'failed') { return ReconciliationResult::failedUnrecoverable( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The backup schedule produced no usable backup set before the run stopped updating.', evidence: $evidence + ['chosen_backup_set_id' => (int) $backupSet->getKey()], related: $related, summaryCounts: $summaryCounts, ); } return ReconciliationResult::notReconciled( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The related backup set is still being prepared and does not prove final backup truth yet.', evidence: $evidence + ['chosen_backup_set_id' => (int) $backupSet->getKey()], related: $related, ); } $schedule = $scheduleId !== null ? BackupSchedule::query()->withTrashed()->whereKey($scheduleId)->where('managed_environment_id', (int) $run->managed_environment_id)->first() : null; if ($schedule instanceof BackupSchedule && $schedule->trashed()) { return ReconciliationResult::blocked( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The backup schedule is archived, so this run never produced a current backup set.', evidence: $evidence, summaryCounts: [ 'total' => 0, 'processed' => 0, 'succeeded' => 0, 'failed' => 0, 'skipped' => 1, ], ); } return ReconciliationResult::notReconciled( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'No current backup set proof is available yet for this backup schedule run.', evidence: $evidence, ); } public function reconcileException(OperationRun $run, Throwable $throwable): ?ReconciliationResult { return null; } /** * @return array{0:BackupSet|null,1:?string} */ private function resolveBackupSet(OperationRun $run, ?int $backupSetId): array { if ($backupSetId === null) { return [null, null]; } $candidate = BackupSet::query()->whereKey($backupSetId)->first(); if (! $candidate instanceof BackupSet) { return [null, null]; } if ((int) $candidate->managed_environment_id !== (int) $run->managed_environment_id) { return [null, 'The recorded backup set no longer matches the queued backup schedule scope safely.']; } return [$candidate, null]; } }