TenantAtlas/app/Models/RestoreRun.php
ahmido a97beefda3 056-remove-legacy-bulkops (#65)
Kurzbeschreibung

Versteckt die Rerun-Row-Action für archivierte (soft-deleted) RestoreRuns und verhindert damit fehlerhafte Neu-Starts aus dem Archiv; ergänzt einen Regressionstest.
Änderungen

Code: RestoreRunResource.php — Sichtbarkeit der rerun-Action geprüft auf ! $record->trashed() und defensive Abbruchprüfung im Action-Handler.
Tests: RestoreRunRerunTest.php — neuer Test rerun action is hidden for archived restore runs.
Warum

Archivierte RestoreRuns durften nicht neu gestartet werden; UI zeigte trotzdem die Option. Das führte zu verwirrendem Verhalten und möglichen Fehlern beim Enqueueing.
Verifikation / QA

Unit/Feature:
./vendor/bin/sail artisan test tests/Feature/RestoreRunRerunTest.php
Stil/format:
./vendor/bin/pint --dirty
Manuell (UI):
Als Tenant-Admin Filament → Restore Runs öffnen.
Filter Archived aktivieren (oder Trashed filter auswählen).
Sicherstellen, dass für archivierte Einträge die Rerun-Action nicht sichtbar ist.
Auf einem aktiven (nicht-archivierten) Run prüfen, dass Rerun sichtbar bleibt und wie erwartet eine neue RestoreRun erzeugt.
Wichtige Hinweise

Kein DB-Migration required.
Diese PR enthält nur den UI-/Filament-Fix; die zuvor gemachten operative Fixes für Queue/adapter-Reconciliation bleiben ebenfalls auf dem Branch (z. B. frühere commits während der Debugging-Session).
T055 (Schema squash) wurde bewusst zurückgestellt und ist nicht Teil dieses PRs.
Merge-Checklist

 Tests lokal laufen (RestoreRunRerunTest grünt)
 Pint läuft ohne ungepatchte Fehler
 Branch gepusht: 056-remove-legacy-bulkops (PR-URL: https://git.cloudarix.de/ahmido/TenantAtlas/compare/dev...056-remove-legacy-bulkops)

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #65
2026-01-19 23:27:52 +00:00

153 lines
4.4 KiB
PHP

<?php
namespace App\Models;
use App\Support\RestoreRunStatus;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class RestoreRun extends Model
{
use HasFactory;
use SoftDeletes;
protected $guarded = [];
protected $casts = [
'is_dry_run' => 'boolean',
'idempotency_key' => 'string',
'requested_items' => 'array',
'preview' => 'array',
'results' => 'array',
'metadata' => 'array',
'group_mapping' => 'array',
'started_at' => 'datetime',
'completed_at' => 'datetime',
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function backupSet(): BelongsTo
{
return $this->belongsTo(BackupSet::class)->withTrashed();
}
public function scopeDeletable(Builder $query): Builder
{
return $query->whereIn('status', array_map(
static fn (RestoreRunStatus $status): string => $status->value,
[
RestoreRunStatus::Draft,
RestoreRunStatus::Scoped,
RestoreRunStatus::Checked,
RestoreRunStatus::Previewed,
RestoreRunStatus::Completed,
RestoreRunStatus::Partial,
RestoreRunStatus::Failed,
RestoreRunStatus::Cancelled,
RestoreRunStatus::Aborted,
RestoreRunStatus::CompletedWithErrors,
]
));
}
public function isDeletable(): bool
{
$status = RestoreRunStatus::fromString($this->status);
return $status?->isDeletable() ?? false;
}
// Group mapping helpers
public function hasGroupMapping(): bool
{
return ! empty($this->group_mapping);
}
public function getMappedGroupId(string $sourceGroupId): ?string
{
$mapping = $this->group_mapping ?? [];
return $mapping[$sourceGroupId] ?? null;
}
public function isGroupSkipped(string $sourceGroupId): bool
{
$mapping = $this->group_mapping ?? [];
return ($mapping[$sourceGroupId] ?? null) === 'SKIP';
}
public function getUnmappedGroupIds(array $sourceGroupIds): array
{
return array_diff($sourceGroupIds, array_keys($this->group_mapping ?? []));
}
public function addGroupMapping(string $sourceGroupId, string $targetGroupId): void
{
$mapping = $this->group_mapping ?? [];
$mapping[$sourceGroupId] = $targetGroupId;
$this->group_mapping = $mapping;
}
// Assignment restore outcome helpers
public function getAssignmentRestoreOutcomes(): array
{
$results = $this->results ?? [];
if (isset($results['assignment_outcomes']) && is_array($results['assignment_outcomes'])) {
return $results['assignment_outcomes'];
}
if (isset($results['items']) && is_array($results['items'])) {
return collect($results['items'])
->pluck('assignment_outcomes')
->flatten(1)
->filter()
->values()
->all();
}
if (! is_array($results)) {
return [];
}
return collect($results)
->pluck('assignment_outcomes')
->flatten(1)
->filter(static fn (mixed $outcome): bool => is_array($outcome))
->values()
->all();
}
public function getSuccessfulAssignmentsCount(): int
{
return count(array_filter(
$this->getAssignmentRestoreOutcomes(),
static fn (mixed $outcome): bool => is_array($outcome) && ($outcome['status'] ?? null) === 'success'
));
}
public function getFailedAssignmentsCount(): int
{
return count(array_filter(
$this->getAssignmentRestoreOutcomes(),
static fn (mixed $outcome): bool => is_array($outcome) && ($outcome['status'] ?? null) === 'failed'
));
}
public function getSkippedAssignmentsCount(): int
{
return count(array_filter(
$this->getAssignmentRestoreOutcomes(),
static fn (mixed $outcome): bool => is_array($outcome) && ($outcome['status'] ?? null) === 'skipped'
));
}
}