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
109 lines
5.4 KiB
Markdown
109 lines
5.4 KiB
Markdown
# 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](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)
|
||
|
||
```text
|
||
specs/052-async-add-policies/
|
||
├── plan.md
|
||
├── spec.md
|
||
├── tasks.md
|
||
└── checklists/
|
||
└── requirements.md
|
||
```
|
||
|
||
### Source Code (planned touch points)
|
||
|
||
```text
|
||
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 (`pending` → `running`).
|
||
2. Validate the backup set exists and belongs to the run tenant.
|
||
3. Process the run’s 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.
|