Implements Spec 110 Ops‑UX Enforcement and applies the repo‑wide “enterprise” standard for operation start + dedup surfaces. Key points - Start surfaces: only ephemeral queued toast (no DB notifications for started/queued/running). - Dedup paths: canonical “already queued” toast. - Progress refresh: dispatch run-enqueued browser event so the global widget updates immediately. - Completion: exactly-once terminal DB notification on completion (per Ops‑UX contract). Tests & formatting - Full suite: 1738 passed, 8 skipped (8477 assertions). - Pint: `vendor/bin/sail bin pint --dirty --format agent` (pass). Notable change - Removed legacy `RunStatusChangedNotification` (replaced by the terminal-only completion notification policy). Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #134
209 lines
7.0 KiB
PHP
209 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Jobs\Middleware\TrackOperationRun;
|
|
use App\Models\BackupItem;
|
|
use App\Models\BackupSet;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\Intune\AuditLogger;
|
|
use App\Services\OperationRunService;
|
|
use App\Support\OpsUx\RunFailureSanitizer;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Throwable;
|
|
|
|
class RemovePoliciesFromBackupSetJob implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public ?OperationRun $operationRun = null;
|
|
|
|
/**
|
|
* @param array<int,int> $backupItemIds
|
|
*/
|
|
public function __construct(
|
|
public int $backupSetId,
|
|
public array $backupItemIds,
|
|
public int $initiatorUserId,
|
|
?OperationRun $operationRun = null,
|
|
) {
|
|
$this->operationRun = $operationRun;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, object>
|
|
*/
|
|
public function middleware(): array
|
|
{
|
|
return [new TrackOperationRun];
|
|
}
|
|
|
|
public function handle(
|
|
AuditLogger $auditLogger,
|
|
): void {
|
|
$backupSet = BackupSet::query()->with(['tenant'])->find($this->backupSetId);
|
|
|
|
if (! $backupSet instanceof BackupSet) {
|
|
if ($this->operationRun) {
|
|
/** @var OperationRunService $opService */
|
|
$opService = app(OperationRunService::class);
|
|
$opService->updateRun(
|
|
$this->operationRun,
|
|
'completed',
|
|
'failed',
|
|
['failed' => 1],
|
|
[['code' => 'backup_set.not_found', 'message' => 'Backup set not found.']]
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$tenant = $backupSet->tenant;
|
|
|
|
$initiator = User::query()->find($this->initiatorUserId);
|
|
|
|
$requestedIds = collect($this->backupItemIds)
|
|
->map(fn (mixed $value): int => (int) $value)
|
|
->filter(fn (int $value): bool => $value > 0)
|
|
->unique()
|
|
->sort()
|
|
->values()
|
|
->all();
|
|
|
|
$requestedCount = count($requestedIds);
|
|
|
|
$failures = [];
|
|
|
|
try {
|
|
/** @var \Illuminate\Database\Eloquent\Collection<int, BackupItem> $items */
|
|
$items = BackupItem::query()
|
|
->where('backup_set_id', $backupSet->getKey())
|
|
->whereIn('id', $requestedIds)
|
|
->get();
|
|
|
|
$foundIds = $items->pluck('id')->map(fn (mixed $value): int => (int) $value)->all();
|
|
$missingIds = array_values(array_diff($requestedIds, $foundIds));
|
|
|
|
foreach ($missingIds as $missingId) {
|
|
$failures[] = [
|
|
'code' => 'backup_item.not_found',
|
|
'message' => RunFailureSanitizer::sanitizeMessage("Backup item {$missingId} not found (already removed?)."),
|
|
];
|
|
}
|
|
|
|
$removed = 0;
|
|
$policyIds = [];
|
|
$policyIdentifiers = [];
|
|
|
|
foreach ($items as $item) {
|
|
$item->delete();
|
|
$removed++;
|
|
|
|
if ($item->policy_id) {
|
|
$policyIds[] = (int) $item->policy_id;
|
|
}
|
|
|
|
if ($item->policy_identifier) {
|
|
$policyIdentifiers[] = (string) $item->policy_identifier;
|
|
}
|
|
}
|
|
|
|
$backupSet->update([
|
|
'item_count' => $backupSet->items()->count(),
|
|
]);
|
|
|
|
if ($tenant instanceof Tenant) {
|
|
$auditLogger->log(
|
|
tenant: $tenant,
|
|
action: 'backup.items_removed',
|
|
resourceType: 'backup_set',
|
|
resourceId: (string) $backupSet->getKey(),
|
|
status: 'success',
|
|
context: [
|
|
'metadata' => [
|
|
'removed_count' => $removed,
|
|
'requested_count' => $requestedCount,
|
|
'missing_count' => count($missingIds),
|
|
'policy_ids' => array_values(array_unique($policyIds)),
|
|
'policy_identifiers' => array_values(array_unique($policyIdentifiers)),
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
'initiator_user_id' => $initiator?->getKey(),
|
|
],
|
|
],
|
|
actorId: $initiator?->getKey(),
|
|
);
|
|
}
|
|
|
|
if ($this->operationRun) {
|
|
/** @var OperationRunService $opService */
|
|
$opService = app(OperationRunService::class);
|
|
|
|
$this->operationRun->update([
|
|
'context' => array_merge($this->operationRun->context ?? [], [
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
'requested_count' => $requestedCount,
|
|
'removed_count' => $removed,
|
|
'missing_count' => count($missingIds),
|
|
'remaining_count' => (int) $backupSet->item_count,
|
|
]),
|
|
]);
|
|
|
|
$outcome = 'succeeded';
|
|
if ($removed === 0) {
|
|
$outcome = 'failed';
|
|
} elseif ($failures !== []) {
|
|
$outcome = 'partially_succeeded';
|
|
}
|
|
|
|
$opService->updateRun(
|
|
$this->operationRun,
|
|
'completed',
|
|
$outcome,
|
|
[
|
|
'total' => $requestedCount,
|
|
'processed' => $requestedCount,
|
|
'succeeded' => $removed,
|
|
'failed' => count($missingIds),
|
|
'deleted' => $removed,
|
|
'items' => $requestedCount,
|
|
],
|
|
$failures,
|
|
);
|
|
}
|
|
|
|
} catch (Throwable $throwable) {
|
|
if ($tenant instanceof Tenant) {
|
|
$auditLogger->log(
|
|
tenant: $tenant,
|
|
action: 'backup.items_removed',
|
|
resourceType: 'backup_set',
|
|
resourceId: (string) $backupSet->getKey(),
|
|
status: 'failed',
|
|
context: [
|
|
'metadata' => [
|
|
'requested_count' => $requestedCount,
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
],
|
|
],
|
|
actorId: $initiator?->getKey(),
|
|
);
|
|
}
|
|
|
|
if ($this->operationRun) {
|
|
/** @var OperationRunService $opService */
|
|
$opService = app(OperationRunService::class);
|
|
$opService->failRun($this->operationRun, $throwable);
|
|
}
|
|
|
|
throw $throwable;
|
|
}
|
|
}
|
|
}
|