$context */ public static function buildKey(int $tenantId, string $operationType, string|int|null $targetId = null, array $context = []): string { $payload = [ 'tenant_id' => $tenantId, 'operation_type' => trim($operationType), 'target_id' => $targetId === null ? null : (string) $targetId, 'context' => self::canonicalize($context), ]; return hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR)); } public static function findActiveBulkOperationRun(int $tenantId, string $idempotencyKey): ?BulkOperationRun { return BulkOperationRun::query() ->where('tenant_id', $tenantId) ->where('idempotency_key', $idempotencyKey) ->whereIn('status', ['pending', 'running']) ->latest('id') ->first(); } public static function findActiveRestoreRun(int $tenantId, string $idempotencyKey): ?RestoreRun { return RestoreRun::query() ->where('tenant_id', $tenantId) ->where('idempotency_key', $idempotencyKey) ->whereIn('status', ['queued', 'running']) ->latest('id') ->first(); } /** * Deterministic idempotency key for a live restore execution. * * @param array|null $selectedItemIds * @param array $groupMapping */ public static function restoreExecuteKey( int $tenantId, int $backupSetId, ?array $selectedItemIds, array $groupMapping = [], ): string { $scopeIds = $selectedItemIds; if (is_array($scopeIds)) { $scopeIds = array_values(array_unique(array_map('intval', $scopeIds))); sort($scopeIds); } return self::buildKey( tenantId: $tenantId, operationType: 'restore.execute', targetId: (string) $backupSetId, context: [ 'scope' => $scopeIds, 'group_mapping' => $groupMapping, ], ); } /** * @param array $value * @return array */ private static function canonicalize(array $value): array { $value = Arr::map($value, function (mixed $item): mixed { if (is_array($item)) { /** @var array $item */ return static::canonicalize($item); } return $item; }); ksort($value); return $value; } }