
# Implementation Plan: BackupSchedule Retention & Lifecycle (Archive/Restore/Force Delete) (Spec 091)

**Branch**: `091-backupschedule-retention-lifecycle` | **Date**: 2026-02-13 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/091-backupschedule-retention-lifecycle/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/091-backupschedule-retention-lifecycle/spec.md`

## Summary

Introduce an enterprise lifecycle for tenant backup schedules:
- **Archive** is the default destructive action (soft delete), removing schedules from the active list and preventing execution.
- **Restore** re-activates archived schedules without changing their enabled/disabled state.
- **Force delete** permanently removes archived schedules, but is strictly capability-gated and **blocked if the schedule has historical runs**.

All lifecycle actions:
- enforce tenant isolation + capability-first RBAC (non-member → 404, member missing capability → 403),
- require confirmation for destructive actions (archive + force delete),
- write stable audit events.

## Technical Context

**Language/Version**: PHP 8.4.15
**Framework**: Laravel 12
**Admin UI**: Filament v5 + Livewire v4.0+
**Storage**: PostgreSQL (Sail)
**Testing**: Pest v4 (`vendor/bin/sail artisan test --compact`)
**Target Platform**: Docker/Sail local; Dokploy staging/prod
**Project Type**: Laravel monolith
**Performance Goals**: Ensure dispatch due schedules remains streaming (`cursor()`), no N+1.
**Constraints**:
- No new dependencies
- No bulk destructive actions for BackupSchedule lifecycle (per spec)
- Archived schedules must never be dispatched or executed

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

- ✅ Inventory-first vs backups: This feature affects backups scheduling only, not inventory state.
- ✅ Read/write separation: Lifecycle mutations are DB-only, confirmed, audited, and tested.
- ✅ Graph contract path: No Graph calls introduced.
- ✅ Deterministic capabilities: Use `App\Support\Auth\Capabilities` constants only.
- ✅ RBAC-UX semantics: non-member tenant access → 404; member missing capability → 403; actions use central helpers.
- ✅ Destructive confirmation: archive + force delete require `->requiresConfirmation()`.
- ✅ Filament Action Surface Contract: list keeps clickable row inspect; max 2 visible row actions; destructive actions grouped under “More”; empty-state CTA exists.

## Project Structure

### Documentation (this feature)

```text
/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/091-backupschedule-retention-lifecycle/
├── spec.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
└── contracts/
    └── backup-schedule-lifecycle-v1.openapi.yaml
```

### Source Code (relevant to this spec)

```text
app/
├── Console/Commands/TenantpilotDispatchBackupSchedules.php
├── Filament/Resources/BackupScheduleResource.php
├── Filament/Resources/BackupScheduleResource/Pages/*
├── Jobs/RunBackupScheduleJob.php
├── Models/BackupSchedule.php
├── Services/BackupScheduling/BackupScheduleDispatcher.php
├── Services/Intune/AuditLogger.php
└── Support/Rbac/UiEnforcement.php

database/migrations/
└── 2026_01_05_011014_create_backup_schedules_table.php

tests/Feature/BackupScheduling/
├── BackupScheduleBulkDeleteTest.php
├── BackupScheduleCrudTest.php
├── DispatchIdempotencyTest.php
├── RunBackupScheduleJobTest.php
└── RunNowRetryActionsTest.php
```

**Structure Decision**: Single Laravel monolith; changes are limited to existing model/migration, the Filament resource/pages, the dispatcher/job safety behavior, and targeted Pest tests.

## Phase 0 — Outline & Research (DOCS COMPLETE)

Completed in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/091-backupschedule-retention-lifecycle/research.md`.

Key findings:
- The repo already uses soft-delete lifecycle patterns in Filament (e.g., Backup Sets) via `TrashedFilter` + custom actions.
- Central RBAC enforcement exists for Filament actions via `App\Support\Rbac\UiEnforcement` (membership-driven hidden UI + capability-driven disabled UI + server-side guard).
- Scheduler dispatch is centralized in `BackupScheduleDispatcher::dispatchDue()` and already uses idempotent `OperationRun` identities.

## Phase 1 — Design & Contracts (DOCS COMPLETE)

Outputs:
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/091-backupschedule-retention-lifecycle/data-model.md`
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/091-backupschedule-retention-lifecycle/contracts/backup-schedule-lifecycle-v1.openapi.yaml`
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/091-backupschedule-retention-lifecycle/quickstart.md`

Design choices:
- Use **Eloquent SoftDeletes** on `BackupSchedule` to represent “Archived”.
- Default list shows active schedules; archived schedules are accessible via a labeled `TrashedFilter`.
- Lifecycle actions are implemented as Filament actions (not controllers) and wrapped via `UiEnforcement`:
  - Archive / Restore require `Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE`.
  - Force delete requires `Capabilities::TENANT_DELETE`.
- Force delete is blocked if historical runs exist. In this codebase, “historical runs” are represented by `OperationRun` records with `context->backup_schedule_id`.
- Scheduler ignores archived schedules by default via SoftDeletes query scoping; job execution path also skips if a schedule is archived between dispatch and execution.

## Phase 1 — Agent Context Update (DONE)

The agent context update script is executed after Phase 1 artifacts are generated:
- `.specify/scripts/bash/update-agent-context.sh copilot`

## Constitution Check (Post-Design)

- ✅ Livewire v4.0+ compliant (Filament v5)
- ✅ No Graph calls introduced
- ✅ Lifecycle actions are confirmed + audited
- ✅ RBAC-UX membership/capability semantics remain enforced by central helpers + existing tenant routing
- ✅ Action Surface Contract remains satisfied (no bulk destructive actions introduced)

## Phase 2 — Implementation Plan

### Step 1 — Add SoftDeletes to BackupSchedule

1) Add `SoftDeletes` trait to `App\Models\BackupSchedule`.
2) Create a migration to add `deleted_at` to `backup_schedules` (with an index that supports common queries, e.g. `(tenant_id, deleted_at)` or similar).

### Step 2 — Update the Filament BackupSchedule Resource to reflect lifecycle

Primary goal: replace “hard delete” semantics with an **Archive / Restore / Force delete** lifecycle, while staying compliant with the Action Surface Contract.

Work items:
- Add `TrashedFilter` to the table filters with “Archived” labels matching the repo conventions.
- Remove/disable bulk destructive actions for schedules (per spec).
- Replace any `DeleteAction` usage for schedules with an “Archive” action that calls `$record->delete()`.
- Add “Restore” action (`$record->restore()`), visible only for archived records, with **no required confirmation**.
- Add “Force delete” action (`$record->forceDelete()`), visible only for archived records, with confirmation required.
- Keep at most 2 visible row actions; group lifecycle actions under a `More` menu (`ActionGroup`).
- Ensure edit remains the primary inspection affordance (`recordUrl()` to Edit).

### Step 3 — Enforce RBAC semantics consistently

- Wrap all lifecycle actions using `UiEnforcement`:
  - Archive/Restore → `Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE`
  - Force delete → `Capabilities::TENANT_DELETE`
- Preserve visibility conditions (e.g., only show Restore/Force delete when `trashed()`).

### Step 4 — Implement force delete blocking (historical runs)

- Define “historical runs exist” as: schedule has `OperationRun` records in `BackupSchedule::operationRuns()`.
- Force delete must fail closed:
  - In UI: show a deterministic danger notification and do not delete.
    - Title: “Cannot force delete backup schedule”
    - Body: “Backup schedules referenced by historical runs cannot be removed.”
  - In server-side action handler: early return without deleting and without writing the `backup_schedule.force_deleted` audit event.

### Step 5 — Audit logging for lifecycle actions

Use `App\Services\Intune\AuditLogger` to emit stable action IDs:
- `backup_schedule.archived`
- `backup_schedule.restored`
- `backup_schedule.force_deleted`

Include tenant context, actor, resource type/id, and metadata (schedule id, schedule name, and an “archived_at/restored_at” timestamp if helpful).

### Step 6 — Operational correctness: archived schedules never execute

- Verify `BackupScheduleDispatcher::dispatchDue()` excludes archived schedules (SoftDeletes default scoping).
- Update `RunBackupScheduleJob` to explicitly detect a schedule that becomes archived after being queued and **skip execution** (no Graph calls):
  - Mark the `OperationRun` as terminal `completed` with outcome `blocked`.
  - Use deterministic summary counts for the skip case (mirrors existing skip patterns): `total=0`, `processed=0`, `failed=0`, `skipped=1`.
  - Add a deterministic failure_summary entry: `code=schedule_archived`, `message=Schedule is archived; run will not execute.`
  - Update the schedule’s `last_run_status` to `skipped` (and `last_run_at` to “now”) so operators can see why the last queued run didn’t execute.
  - Emit the existing operational audit event `backup_schedule.run_skipped` with `reason=schedule_archived` (consistent with the existing `concurrent_run` skip path).

### Step 7 — Testing plan (Pest)

Minimum programmatic verification (update existing tests where applicable):
- Archive schedule: sets `deleted_at`, excludes from active list, audit event written.
- Restore schedule: clears `deleted_at` and does not change `is_enabled`, audit event written.
- Force delete:
  - only available when archived
  - requires `Capabilities::TENANT_DELETE`
  - blocked when historical runs exist
  - audit event written on success
- Scheduler safety: archived schedules are not dispatched.

Expected test updates:
- `tests/Feature/BackupScheduling/BackupScheduleBulkDeleteTest.php` must be removed or rewritten, since bulk destructive actions are forbidden by Spec 091.

### Step 8 — Formatting

- `vendor/bin/sail bin pint --dirty`
