# Tasks: Tenant Review Pack Export v1 (CSV + ZIP)

**Input**: Design documents from `/specs/109-review-pack-export/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/api-contracts.md, quickstart.md

**Tests**: Required (Pest Feature tests). Seven test files covering generation, download, RBAC, prune, resource, widget, and schedule.
**Operations**: `OperationRun` of type `tenant.review_pack.generate` tracks all generation runs. Failure `reason_code` stored in `context` jsonb column. Active-run dedupe via application-level guard (`scopeActive()`).
**RBAC**:
- Gate/Policy enforcement: `REVIEW_PACK_VIEW` (list + download), `REVIEW_PACK_MANAGE` (generate + expire).
- Non-member → 404 (deny-as-not-found, workspace/tenant isolation). Member missing capability → 403.
- Capabilities registered in canonical `Capabilities.php` registry — no raw strings in feature code.
- Authorization plane: tenant-context (`/admin/t/{tenant}/...`).
- ReviewPackResource does NOT participate in global search (intentionally excluded; no `$recordTitleAttribute`).
- Destructive actions (Expire) use `->requiresConfirmation()`.
- Tests include positive + negative authorization scenarios.
**Filament UI Action Surfaces**: UI Action Matrix in spec.md fulfilled. List: header "Generate Pack" modal, row "Download" + "Expire" (max 2 visible), empty state CTA. View: header "Download" + "Regenerate". Dashboard card: "Generate" + "Download" conditional.
**Filament UI UX-001**: View page uses Infolist (not disabled edit form). All modal fields inside Sections. Status badges use BADGE-001. Empty state has title + explanation + 1 CTA. Clickable rows via `recordUrl()`.
**Badges**: `ReviewPackStatus` domain added to `BadgeDomain` enum with `ReviewPackStatusBadge` mapper per BADGE-001. Tests assert each status renders correct badge.

**Organization**: Tasks grouped by user story per spec.md priorities (P1 → P2 → P3).

## Format: `[ID] [P?] [Story] Description`

- **[P]**: Can run in parallel (different files, no dependencies on incomplete tasks)
- **[Story]**: Which user story (US1–US5) this task belongs to
- Include exact file paths in descriptions

---

## Phase 1: Setup

**Purpose**: Database schema — must complete before model/factory creation

- [X] T001 Create migration for `review_packs` table with all columns (id, workspace_id FK, tenant_id FK, operation_run_id FK nullable, initiated_by_user_id FK nullable, status, fingerprint, previous_fingerprint, summary jsonb, options jsonb, file_disk, file_path, file_size bigint, sha256, generated_at timestampTz, expires_at timestampTz, timestamps), three indexes, and partial unique index `WHERE fingerprint IS NOT NULL AND status NOT IN ('expired','failed')` in `database/migrations/XXXX_create_review_packs_table.php`

---

## Phase 2: Foundational (Blocking Prerequisites)

**Purpose**: Core enums, config, capabilities, badges, model, and factory that ALL user stories depend on

**⚠️ CRITICAL**: No user story work can begin until this phase is complete

- [X] T002 [P] Create `ReviewPackStatus` enum with 5 string-backed cases (Queued, Generating, Ready, Failed, Expired) in `app/Support/ReviewPackStatus.php`
- [X] T003 [P] Add `ReviewPackGenerate` case with value `tenant.review_pack.generate` to `app/Support/OperationRunType.php`
- [X] T004 [P] Add `REVIEW_PACK_VIEW = 'review_pack.view'` and `REVIEW_PACK_MANAGE = 'review_pack.manage'` constants to `app/Support/Auth/Capabilities.php`
- [X] T005 [P] Add `ReviewPackStatus = 'review_pack_status'` case to `app/Support/Badges/BadgeDomain.php`
- [X] T006 [P] Create `ReviewPackStatusBadge` mapper returning `BadgeSpec` for 5 statuses (queued→warning, generating→info, ready→success, failed→danger, expired→gray) in `app/Support/Badges/Mappers/ReviewPackStatusBadge.php`
- [X] T007 [P] Add `exports` disk with local driver at `storage_path('app/private/exports')`, `serve => false`, `throw => true` to `config/filesystems.php`
- [X] T008 [P] Add `review_pack` config section with retention_days (90), hard_delete_grace_days (30), download_url_ttl_minutes (60), include_pii_default (true), include_operations_default (true) to `config/tenantpilot.php`
- [X] T009 Create `ReviewPack` model with `DerivesWorkspaceIdFromTenant` + `HasFactory` traits, `guarded = []`, casts (summary/options→array, generated_at/expires_at→datetime, file_size→integer), relationships (workspace, tenant, operationRun, initiator via initiated_by_user_id), scopes (ready, expired, pastRetention, forTenant, latestReady), and STATUS_* constants in `app/Models/ReviewPack.php`
- [X] T010 Create `ReviewPackFactory` with default definition (ready state) and 5 named states (queued, generating, ready, failed, expired) that set correct status + nullable file fields per state in `database/factories/ReviewPackFactory.php`
- [X] T011 Run migration and verify model + factory (`vendor/bin/sail artisan migrate && vendor/bin/sail artisan test --compact --filter=ReviewPack`)

**Checkpoint**: Foundation ready — user story implementation can now begin

---

## Phase 3: User Story 1 — Generate and Download Review Pack (Priority: P1) 🎯 MVP

**Goal**: Produce an auditable ZIP artifact (summary.json, findings.csv, operations.csv, hardening.json, reports/*.json, metadata.json) from cached DB data, store privately on exports disk, deliver via signed URL, and provide full Filament UI.

**Independent Test**: Create tenant with stored reports + findings, trigger generation as REVIEW_PACK_MANAGE user, assert OperationRun created, review_packs row transitions to ready, ZIP file exists on exports disk with correct 7-file set, download via signed URL returns correct headers and content.

### Implementation for User Story 1

- [X] T012 [US1] Implement `ReviewPackService` with `generate(Tenant, User, array $options): ReviewPack` (creates OperationRun + ReviewPack + dispatches job), `computeFingerprint(Tenant, array $options): string` (SHA-256 of tenant_id + options + report fingerprints + max finding last_seen_at + hardening tuple), `generateDownloadUrl(ReviewPack): string` (URL::signedRoute with configurable TTL), `findExistingPack(Tenant, string $fingerprint): ?ReviewPack` (ready + unexpired dedupe check), and `checkActiveRun(Tenant): bool` (active OperationRun guard) in `app/Services/ReviewPackService.php`
- [X] T013 [US1] Implement `GenerateReviewPackJob` (ShouldQueue) with 12-step pipeline: load records, mark running, collect StoredReports (permission_posture + entra.admin_roles), collect Findings (status in open/acknowledged, chunked), collect tenant hardening fields (via accessor, not raw DB), collect recent OperationRuns (30 days), compute data_freshness, build file map with PII redaction when include_pii=false, assemble ZIP via ZipArchive (alphabetical order, temp file), compute SHA-256, store on exports disk, update ReviewPack (status=ready, fingerprint, sha256, file_size, file_path, file_disk, generated_at, summary, expires_at), mark OperationRun completed; on failure: mark failed with reason_code in context, notify initiator in `app/Jobs/GenerateReviewPackJob.php`
- [X] T014 [P] [US1] Create `ReviewPackStatusNotification` (database channel) with conditional rendering: ready payload (title, body with tenant name, View action URL) and failed payload (title, body with sanitized reason, View action URL) in `app/Notifications/ReviewPackStatusNotification.php`
- [X] T015 [US1] Create `ReviewPackDownloadController` with single `__invoke(Request, ReviewPack)` method: validate status is ready, stream file via `Storage::disk($pack->file_disk)->download()` with headers Content-Type application/zip, Content-Disposition attachment with filename `review-pack-{tenant_external_id}-{YYYY-MM-DD}.zip`, X-Review-Pack-SHA256; return 404 for expired/non-ready packs in `app/Http/Controllers/ReviewPackDownloadController.php`
- [X] T016 [US1] Add signed download route `GET /admin/review-packs/{reviewPack}/download` named `admin.review-packs.download` with `signed` middleware in `routes/web.php`
- [X] T017 [US1] Create `ReviewPackResource` with: table columns (status badge via BADGE-001, tenant name, generated_at datetime, expires_at datetime, file_size formatted), `recordUrl()` for clickable rows to ViewReviewPack, header action "Generate Pack" (modal with Section containing include_pii Toggle + include_operations Toggle, authorized by REVIEW_PACK_MANAGE, calls ReviewPackService::generate), row actions "Download" (visible when ready, REVIEW_PACK_VIEW, openUrlInNewTab with signed URL) + "Expire" (REVIEW_PACK_MANAGE, color danger, requiresConfirmation, sets status=expired + deletes file), empty state with title "No review packs yet" + description + "Generate first pack" CTA, filters (status SelectFilter, generated_at date range), search on tenant name, sort on generated_at/status, nav group Monitoring → Exports in `app/Filament/Resources/ReviewPackResource.php`
- [X] T018 [US1] Create `ListReviewPacks` page extending ListRecords in `app/Filament/Resources/ReviewPackResource/Pages/ListReviewPacks.php`
- [X] T019 [US1] Create `ViewReviewPack` page with Infolist layout: status badge (BADGE-001), summary section (data_freshness per-source timestamps, generated_at, expires_at, file_size, sha256), options section (include_pii, include_operations), initiator + OperationRun link; header actions "Download" (REVIEW_PACK_VIEW, visible when ready) + "Regenerate" (REVIEW_PACK_MANAGE, requiresConfirmation when ready pack exists, pre-fills current options, sets previous_fingerprint) in `app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php`
- [X] T020 [P] [US1] Create `TenantReviewPackCard` widget with 5 display states: no pack ("No review pack yet" + Generate CTA), queued/generating (status badge + "Generation in progress"), ready (badge + generated_at + expires_at + file_size + Download action REVIEW_PACK_VIEW + "Generate new" REVIEW_PACK_MANAGE), failed (badge + sanitized reason + Retry=Generate REVIEW_PACK_MANAGE), expired (badge + expiry date + "Generate new" REVIEW_PACK_MANAGE); data = latest ReviewPack for current tenant in `app/Filament/Widgets/TenantReviewPackCard.php`
- [X] T021 [P] [US1] Create generation tests: happy path (job processes, status→ready, file on disk, sha256+file_size populated, notification sent to initiator, OperationRun completed+success), failure path (exception → status→failed, OperationRun failed, reason_code in context, failure notification), empty reports (job succeeds with empty report sections, status→ready), PII redaction (include_pii=false → display_name replaced with placeholder, UUIDs retained), ZIP contents (exactly 7 files in alphabetical order: findings.csv, hardening.json, metadata.json, operations.csv, reports/entra_admin_roles.json, reports/permission_posture.json, summary.json), performance baseline (seed 1,000 findings + 10 stored reports and assert job completes within 60s per SC-001) in `tests/Feature/ReviewPack/ReviewPackGenerationTest.php`
- [X] T022 [P] [US1] Create download tests: signed URL → 200 with Content-Type application/zip + Content-Disposition + X-Review-Pack-SHA256 headers; expired signature → 403; expired pack (status=expired) → 404; non-ready pack (status=queued) → 404; non-existent pack → 404 in `tests/Feature/ReviewPack/ReviewPackDownloadTest.php`
- [X] T023 [P] [US1] Create resource Livewire tests: list page renders with columns, empty state shows CTA, generate action modal opens with toggle fields, view page displays infolist sections with correct data, expire action requires confirmation and updates status in `tests/Feature/ReviewPack/ReviewPackResourceTest.php`
- [X] T024 [P] [US1] Create widget Livewire tests: no-pack state shows generate CTA, ready state shows download + generate actions, generating state shows in-progress message, failed state shows retry action, expired state shows generate action in `tests/Feature/ReviewPack/ReviewPackWidgetTest.php`

**Checkpoint**: User Story 1 fully functional — can generate, download, view, and manage review packs

---

## Phase 4: User Story 2 — Fingerprint Dedupe (Priority: P2)

**Goal**: Prevent duplicate pack generation; reuse existing ready pack with same fingerprint; reject concurrent generation for same tenant.

**Independent Test**: Trigger generation twice with identical inputs, assert only one ready ReviewPack row and one file on disk.

### Implementation for User Story 2

- [X] T025 [US2] Add fingerprint dedupe integration tests: identical inputs → returns existing ready pack (no new row, no new file), active OperationRun for same tenant → rejection with "generation already in progress" notification, expired pack with same fingerprint → allows new generation (partial unique index excludes expired), different options (include_pii toggled) → new pack with different fingerprint, fingerprint computation is deterministic (same inputs → same hash) in `tests/Feature/ReviewPack/ReviewPackGenerationTest.php`

**Checkpoint**: Fingerprint dedupe verified — duplicate requests handled correctly

---

## Phase 5: User Story 3 — RBAC Enforcement (Priority: P2)

**Goal**: Non-members get 404 (deny-as-not-found), view-only members can list + download but not generate, manage members can generate + expire.

**Independent Test**: Test each role (no membership, REVIEW_PACK_VIEW, REVIEW_PACK_MANAGE) against each action (list, view, download, generate, expire) and assert correct HTTP status or Filament authorization result.

### Implementation for User Story 3

- [X] T026 [US3] Create `ReviewPackPolicy` with `viewAny`, `view`, `create`, `delete` methods mapping to `REVIEW_PACK_VIEW` (viewAny, view) and `REVIEW_PACK_MANAGE` (create, delete) capability checks via canonical registry; enforce workspace_id + tenant_id scope (non-member → null = 404 semantics) in `app/Policies/ReviewPackPolicy.php`
- [X] T027 [US3] Create comprehensive RBAC enforcement tests: non-member → 404 on list page, non-member → 404 on view page, non-member → 404 on download route; REVIEW_PACK_VIEW member → list succeeds, view succeeds, download signed URL succeeds, generate action hidden/403; REVIEW_PACK_MANAGE member → generate succeeds, expire succeeds, download succeeds; signed URL without valid signature → 403; expire action shows requiresConfirmation dialog before execution in `tests/Feature/ReviewPack/ReviewPackRbacTest.php`

**Checkpoint**: RBAC enforcement verified across all surfaces (resource, controller, widget)

---

## Phase 6: User Story 4 — Retention & Prune (Priority: P3)

**Goal**: Auto-expire packs past retention deadline, delete storage files, optionally hard-delete DB rows after grace period. Clean up dead `sla_due` AlertRule option.

**Independent Test**: Create ReviewPack with `expires_at` in past, run prune command, verify status=expired and file deleted from exports disk.

### Implementation for User Story 4

- [X] T028 [US4] Create `PruneReviewPacksCommand` with signature `tenantpilot:review-pack:prune {--hard-delete}`: query ready packs where expires_at < now → set status=expired + delete file from exports disk; when --hard-delete: query expired packs where updated_at < now - grace_days → hard-delete rows; output summary "{n} packs expired, {m} packs hard-deleted" in `app/Console/Commands/PruneReviewPacksCommand.php`
- [X] T029 [US4] Add prune command schedule entry `tenantpilot:review-pack:prune` with `daily()` + `withoutOverlapping()` in `routes/console.php`
- [X] T030 [P] [US4] Remove or hide `sla_due` event_type option from AlertRuleResource form dropdown (keep EVENT_SLA_DUE constant on model for backward compatibility) in `app/Filament/Resources/AlertRuleResource.php`
- [X] T031 [US4] Create prune tests: past retention → status=expired + file deleted from disk; future retention → unaffected; --hard-delete past grace_days → rows removed from DB; --hard-delete within grace_days → rows kept; command output includes correct counts; AlertRule form no longer shows sla_due option in dropdown in `tests/Feature/ReviewPack/ReviewPackPruneTest.php`

**Checkpoint**: Retention automation verified — packs expire and files cleaned up automatically on schedule

---

## Phase 7: User Story 5 — Scheduled Scan Wiring (Priority: P3)

**Goal**: Ensure daily permission-posture and Entra admin-roles scans are wired in the scheduler so review packs consume fresh cached data.

**Independent Test**: Assert the Laravel console schedule contains the expected dispatch entries with at least daily frequency.

### Implementation for User Story 5

- [X] T032 [US5] Verify existing Entra admin roles schedule closure in `routes/console.php` runs daily; add TODO comment for `tenantpilot:posture:dispatch` command (creation deferred per research.md — command infrastructure does not yet exist; FR-015 partially fulfilled)
- [X] T033 [US5] Create schedule assertion test: Entra admin roles dispatch appears with daily frequency; document posture:dispatch deferral as explicit test skip with rationale in `tests/Feature/ReviewPack/ReviewPackScheduleTest.php`

**Checkpoint**: Scheduled scan wiring verified or documented as deferred

---

## Phase 8: Polish & Cross-Cutting Concerns

**Purpose**: Code quality, formatting, and end-to-end validation

- [X] T034 [P] Run Pint formatter on all new and modified files (`vendor/bin/sail bin pint --dirty --format agent`)
- [X] T035 Run full ReviewPack test suite and fix any failures (`vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/`)
- [X] T036 Validate quickstart.md scenarios end-to-end: create tenant with stored reports + findings, generate pack, verify ZIP contents (7 files), download via signed URL, expire pack, run prune command, confirm file deleted

---

## Dependencies & Execution Order

### Phase Dependencies

- **Setup (Phase 1)**: No dependencies — can start immediately
- **Foundational (Phase 2)**: Depends on Phase 1 (migration must exist) — BLOCKS all user stories
- **US1 (Phase 3)**: Depends on Phase 2 — delivers complete MVP
- **US2 (Phase 4)**: Depends on Phase 3 (needs generate pipeline to test dedupe)
- **US3 (Phase 5)**: Depends on Phase 3 (needs resource + controller to test RBAC)
- **US4 (Phase 6)**: Depends on Phase 2 only (prune operates on model, not UI) — can parallel with US1
- **US5 (Phase 7)**: Depends on Phase 2 only — can parallel with US1
- **Polish (Phase 8)**: Depends on all desired phases being complete

### User Story Dependencies

- **US1 (P1)**: Core MVP — blocked only by Foundational phase
- **US2 (P2)**: Tests dedupe behavior in generate pipeline — depends on US1
- **US3 (P2)**: Tests RBAC across all surfaces — depends on US1
- **US4 (P3)**: Prune command + schedule — can start after Foundational (parallel with US1 if desired)
- **US5 (P3)**: Schedule wiring — can start after Foundational (parallel with US1 if desired)

### Within Each User Story

- Service (T012) before Job (T013) — job calls service methods
- Controller (T015) + Route (T016) before Resource download action (T017)
- Resource (T017) before Pages (T018, T019)
- All implementations before their corresponding tests
- Service (T012) before Widget (T020)

### Parallel Opportunities

- **Phase 2**: All tasks T002–T008 run in parallel (7 independent files)
- **Phase 3**: T014 (notification) parallels T013 (job); T020 (widget) parallels T017–T019 (resource/pages) after T012 (service) completes
- **Phase 3 tests**: T021–T024 all run in parallel (4 independent test files)
- **Cross-phase**: US4 (T028–T031) can parallel with US1 (T012–T024) if team capacity allows
- **Cross-phase**: US5 (T032–T033) can parallel with US1

---

## Parallel Example: Foundational Phase

```bash
# Launch all parallel foundational tasks together:
T002: ReviewPackStatus enum       → app/Support/ReviewPackStatus.php
T003: OperationRunType case       → app/Support/OperationRunType.php
T004: Capabilities constants      → app/Support/Auth/Capabilities.php
T005: BadgeDomain case            → app/Support/Badges/BadgeDomain.php
T006: ReviewPackStatusBadge       → app/Support/Badges/Mappers/ReviewPackStatusBadge.php
T007: exports disk config         → config/filesystems.php
T008: review_pack config          → config/tenantpilot.php

# Then sequentially (dependency chain):
T009: ReviewPack model            → app/Models/ReviewPack.php (needs T002)
T010: ReviewPackFactory           → database/factories/ReviewPackFactory.php (needs T009)
T011: Migrate + verify
```

---

## Parallel Example: US1 Tests

```bash
# After all US1 implementations complete, launch test files in parallel:
T021: ReviewPackGenerationTest    → tests/Feature/ReviewPack/ReviewPackGenerationTest.php
T022: ReviewPackDownloadTest      → tests/Feature/ReviewPack/ReviewPackDownloadTest.php
T023: ReviewPackResourceTest      → tests/Feature/ReviewPack/ReviewPackResourceTest.php
T024: ReviewPackWidgetTest        → tests/Feature/ReviewPack/ReviewPackWidgetTest.php
```

---

## Implementation Strategy

### MVP First (User Story 1 Only)

1. Complete Phase 1: Setup (migration)
2. Complete Phase 2: Foundational (model, enums, config, badge)
3. Complete Phase 3: User Story 1 (service, job, controller, resource, widget, tests)
4. **STOP and VALIDATE**: Run `vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/`
5. Deploy/demo if ready — full generate + download + UI works

### Incremental Delivery

1. Setup + Foundational → Foundation ready
2. Add US1 → Generate + Download + UI works → Deploy MVP
3. Add US2 → Dedupe verified → Add US3 → RBAC hardened → Deploy
4. Add US4 → Retention automation → Add US5 → Schedule wiring → Deploy
5. Polish → Final validation → Production-ready

### Parallel Team Strategy

With multiple developers:

1. Team completes Setup + Foundational together
2. Once Foundational is done:
   - Developer A: US1 (core pipeline + UI)
   - Developer B: US4 (prune command — independent of UI)
3. After US1 complete:
   - Developer A: US2 + US3 (dedupe + RBAC tests)
   - Developer B: US5 (schedule wiring)
4. Polish phase together

---

## Notes

- [P] tasks = different files, no dependencies on incomplete tasks
- [Story] label maps task to specific user story for traceability
- Tests are REQUIRED per project conventions (Pest Feature tests)
- FR-015 (`tenantpilot:posture:dispatch`) partially deferred — command infrastructure does not yet exist (documented in research.md §7)
- FR-016 (`sla_due` cleanup) is a minor AlertRule form change in US4 phase
- ReviewPackResource does NOT participate in global search (intentional, per plan.md Filament v5 contract §3)
- All destructive actions (Expire) use `->requiresConfirmation()` per Filament v5 blueprint §9
- No custom frontend assets — standard Filament components only
- Commit after each task or logical group; stop at any checkpoint to validate independently
