TenantAtlas/specs/091-backupschedule-retention-lifecycle/research.md
2026-02-14 14:43:53 +01:00

55 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 Filaments `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 constitutions 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`.