# Research — Spec 091 (BackupSchedule Retention & Lifecycle) ## Decisions ### Decision 1 — Use Eloquent SoftDeletes for “Archived” state - **Chosen**: Add `SoftDeletes` to `App\Models\BackupSchedule` and add `deleted_at` to `backup_schedules`. - **Rationale**: The repo already models reversible “archive” lifecycles using SoftDeletes (e.g., `BackupSet`) and Filament’s `TrashedFilter`. SoftDeletes also makes “archived schedules never execute” the default behavior in scheduler queries. - **Alternatives considered**: - `archived_at` boolean/timestamp field → rejected (would require manual scoping in every query and more room for mistakes). ### Decision 2 — Archived schedules MUST never be dispatched nor executed - **Chosen**: - Scheduler dispatch path (`BackupScheduleDispatcher::dispatchDue`) relies on default SoftDeletes query scoping (archived schedules excluded). - Job execution path (`RunBackupScheduleJob`) will add an explicit guard to skip if the schedule is archived at execution time. - **Rationale**: Prevents race conditions where a schedule is archived after dispatch but before job execution. - **Alternatives considered**: - Only guard in dispatcher → rejected (not sufficient for race conditions). ### Decision 3 — Reuse central RBAC enforcement helpers for lifecycle actions - **Chosen**: Wrap Filament actions using `App\Support\Rbac\UiEnforcement`. - **Rationale**: `UiEnforcement` implements the constitution’s RBAC-UX semantics (non-member hidden + server guard; member missing capability disabled + server guard) and validates capabilities against the canonical registry. - **Alternatives considered**: - Inline `->visible()`, `->disabled()`, `->authorize()` scattered in the resource → rejected (drift + violates “central helper” guidance). ### Decision 4 — Force delete capability and constraints - **Chosen**: - Force delete is available only when the schedule is archived. - Force delete is gated by `Capabilities::TENANT_DELETE` (stricter than manage). - Force delete is blocked if historical runs exist. - **Rationale**: Matches the spec clarification: permanent deletion must be highly privileged and must not break historical traceability. - **Alternatives considered**: - Allow force delete with cascading deletion of historical runs → rejected (breaks auditability). ### Decision 5 — Define “historical runs exist” using existing operational records - **Chosen**: Treat “historical runs exist” as: `BackupSchedule::operationRuns()->exists()`. - **Rationale**: In this repo, the scheduler and run lifecycle already track schedule activity via `OperationRun` with `context->backup_schedule_id`. - **Alternatives considered**: - Use `backup_schedule_runs` table → not currently used by the scheduling runtime (it exists in migrations but the runtime path is `OperationRun`). ### Decision 6 — Audit logging via existing tenant audit logger - **Chosen**: Use `App\Services\Intune\AuditLogger` with stable action identifiers: - `backup_schedule.archived` - `backup_schedule.restored` - `backup_schedule.force_deleted` - **Rationale**: The repo already centralizes audit logging and sanitizes metadata. ## Repo Evidence (Key Findings) - Scheduling dispatch is centralized in `App\Services\BackupScheduling\BackupScheduleDispatcher` and streams schedules via `cursor()`. - Existing tenant capabilities include: - `Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE` - `Capabilities::TENANT_DELETE` - Filament already uses SoftDeletes patterns with `TrashedFilter` and lifecycle actions in `BackupSetResource`.