TenantAtlas/specs/052-async-add-policies/plan.md
ahmido c60d16ffba feat/052-async-add-policies (#59)
Status Update

Committed the async “Add selected” flow: job-only handler, deterministic run reuse, sanitized failure tracking, observation updates, and the new BulkOperationService/Progress test coverage.
All relevant tasks in tasks.md are marked done, and the checklist under requirements.md is fully satisfied (PASS).
Ran ./vendor/bin/pint --dirty plus BackupSetPolicyPickerTableTest.php—all green.

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #59
2026-01-15 22:20:16 +00:00

5.4 KiB
Raw Blame History

Implementation Plan: Async “Add Policies” to Backup Set (052)

Branch: feat/052-async-add-policies | Date: 2026-01-14 | Spec: specs/052-async-add-policies/spec.md Input: Feature specification from /specs/052-async-add-policies/spec.md

Summary

Convert Backup Set → Add Policies (“Add selected”) to job-only execution by creating/reusing a BulkOperationRun, dispatching a queued job to do Graph/snapshot work, and returning immediately with observable feedback and a “View run” link.

Technical Context

Language/Version: PHP 8.4 (Laravel 12)
Admin UI: Filament v4 + Livewire v3
Storage: PostgreSQL
Queue: Laravel queues (Sail-first locally)
Testing: Pest v4
Constraints: No Graph calls during interactive request/response; tenant isolation; idempotent dedupe; safe error persistence.

Constitution Check

  • Inventory-first: this feature updates backup storage (explicit snapshot capture), never “inventory on render”.
  • Read/write separation: interactive action is “write intent only” (enqueue); job performs DB writes with auditability.
  • Graph contract path: any Graph reads happen only inside the job via GraphClientInterface (indirectly via existing services).
  • Deterministic capabilities: selection hashing and idempotency key are deterministic.
  • Tenant isolation: run records and backup set mutations are scoped to the active tenant.
  • Automation: job has run record lifecycle + counts + failures; dedupe prevents duplicates.
  • Data minimization: failures and notifications store sanitized messages only.

Project Structure

Documentation (this feature)

specs/052-async-add-policies/
├── plan.md
├── spec.md
├── tasks.md
└── checklists/
    └── requirements.md

Source Code (planned touch points)

app/Livewire/BackupSetPolicyPickerTable.php        # “Add selected” handler becomes enqueue-only
app/Jobs/AddPoliciesToBackupSetJob.php            # new queued job (name may vary)
app/Models/BulkOperationRun.php                   # reused run record
app/Services/BulkOperationService.php             # reused counters + failure sanitization
app/Support/RunIdempotency.php                    # reused deterministic key builder + active-run lookup
app/Filament/Resources/BulkOperationRunResource.php  # existing run UI used for “View run”
tests/Feature/Filament/BackupSetPolicyPickerTableTest.php  # updated for async behavior
tests/Feature/*                                   # new/updated tests for idempotency + tenant isolation

Execution Model

Action Handler (UI request)

Responsibilities (must remain fast):

  1. Resolve tenant + user context and authorize “add policies to backup set”.
  2. Convert selected policy records into an ID list (no Graph/snapshot work).
  3. Compute a deterministic idempotency key using:
    • tenant id
    • target backup_set_id
    • operation type (e.g., backup_set.add_policies)
    • context payload containing (sorted) policy IDs + option flags (include assignments/scope tags/foundations)
  4. Reuse an active run if one exists; otherwise create a new BulkOperationRun with resource=backup_set, action=add_policies, item_ids = selected policy IDs, total_items = count, and idempotency_key set.
  5. Dispatch a queued job (always async; no sync shortcut) with the run id + backup_set_id + option flags.
  6. Return immediately with a Filament notification and a “View run” link (DB notification preferred).

Queued Job

Responsibilities (all heavy work):

  1. Load the BulkOperationRun and ensure it is still active (pendingrunning).
  2. Validate the backup set exists and belongs to the run tenant.
  3. Process the runs stored policy IDs deterministically (sequentially or chunked):
    • Add/capture each policy into the backup set using existing capture services.
    • Update counts and record per-item failures with safe reasons.
    • Continue through all items (no circuit-breaker abort).
  4. Complete the run with:
    • completed if no failures
    • completed_with_errors (partial) if some failures
    • failed if the job cannot proceed (e.g., backup set missing)
  5. Emit a DB notification to the initiating user summarizing the outcome and linking to the run.

Idempotency & Concurrency

  • Primary dedupe mechanism: RunIdempotency::findActiveBulkOperationRun(tenant_id, idempotency_key).
  • Secondary guard: existing partial unique index on (tenant_id, idempotency_key) for active statuses.
  • Race handling: if concurrent submissions collide, prefer “find existing run and redirect” over throwing.
  • Dedupe scope: tenant-wide (idempotency key does not include user_id).

Testing Strategy (Pest)

Minimum required tests for 052:

  • Action dispatch test: Add selected queues the job and creates/reuses a run.
  • Fail-hard Graph guard test: binding GraphClientInterface (or a capture service) to a mock that must not be called during the action.
  • Idempotency test: calling the action twice with the same selection queues only one job and creates only one active run.
  • Tenant isolation test: run view under another tenant context is forbidden (403).

Target commands:

  • ./vendor/bin/sail artisan test --filter=BackupSetAddPolicies
  • ./vendor/bin/pint --dirty

Rollout Notes

  • Requires queue workers running for background processing (Sail locally; Dokploy workers in staging/prod).
  • No destructive migrations expected; if schema is extended for better observability, it must remain backwards compatible.