# Quickstart: 109 — Tenant Review Pack Export v1 **Date**: 2026-02-23 **Branch**: `109-review-pack-export` --- ## Prerequisites - Sail services running (`vendor/bin/sail up -d`) - Database migrated to latest - At least one workspace with a connected tenant - Tenant has stored reports (permission_posture and/or entra.admin_roles) and findings --- ## Implementation Phases ### Phase 1: Foundation (Data Layer + Config) **Goal:** Database table, model, factory, enum additions, config entries. 1. **Migration**: `create_review_packs_table` - See [data-model.md](data-model.md) for full schema - Includes partial unique index on `(workspace_id, tenant_id, fingerprint)` for non-expired/failed packs 2. **Model**: `ReviewPack` - Uses `DerivesWorkspaceIdFromTenant` - Casts: summary/options as array, generated_at/expires_at as datetime - Relationships: workspace, tenant, operationRun, initiator - Scopes: ready, expired, pastRetention, forTenant, latestReady 3. **Factory**: `ReviewPackFactory` with states: queued, generating, ready, failed, expired 4. **Enum**: Add `ReviewPackGenerate` to `OperationRunType` 5. **Enum**: Add `ReviewPackStatus` (standalone enum at `App\Support\ReviewPackStatus`) 6. **Config**: - `config/filesystems.php` → `exports` disk - `config/tenantpilot.php` → `review_pack` section 7. **Capabilities**: Add `REVIEW_PACK_VIEW` and `REVIEW_PACK_MANAGE` to `Capabilities.php` 8. **Badge**: Add `ReviewPackStatus` to `BadgeDomain` + create `ReviewPackStatusBadge` mapper **Run**: `vendor/bin/sail artisan migrate && vendor/bin/sail artisan test --compact --filter=ReviewPack` --- ### Phase 2: Service Layer + Job **Goal:** Generation logic, ZIP assembly, fingerprint computation. 1. **Service**: `ReviewPackService` - `generate(Tenant, User, array $options): ReviewPack` — orchestrates creation - `computeFingerprint(Tenant, array $options): string` — deterministic hash - `generateDownloadUrl(ReviewPack): string` — signed URL - `findExistingPack(Tenant, string $fingerprint): ?ReviewPack` — dedupe check 2. **Job**: `GenerateReviewPackJob` - Collects data from StoredReport, Finding, Tenant, OperationRun - Builds file map → assembles ZIP → stores on exports disk - Updates ReviewPack status + metadata - Sends notification on completion/failure 3. **Notification**: `ReviewPackStatusNotification` - Database channel - Ready: includes view page link - Failed: includes sanitized reason **Run**: `vendor/bin/sail artisan test --compact --filter=GenerateReviewPack` --- ### Phase 3: Download Controller **Goal:** Signed URL download endpoint. 1. **Controller**: `ReviewPackDownloadController` - Single `__invoke` method - Validates pack exists + status is ready - Streams file with proper headers (Content-Type, Content-Disposition, SHA256) 2. **Route**: `GET /admin/review-packs/{reviewPack}/download` - Named `admin.review-packs.download` - Middleware: `signed` **Run**: `vendor/bin/sail artisan test --compact --filter=ReviewPackDownload` --- ### Phase 4: Filament UI **Goal:** Resource, view page, dashboard widget. 1. **Resource**: `ReviewPackResource` - List page: table with status badge, generated_at, expires_at, file_size - Header action: "Generate Pack" (modal with PII/operations toggles) - Row actions: Download (ready), Expire (destructive + confirmation) - Empty state: "No review packs yet" + "Generate first pack" CTA - Filters: status, date range 2. **View Page**: `ViewReviewPack` - Infolist layout with sections - Header actions: Download, Regenerate - Summary display, data freshness table, options used 3. **Widget**: `TenantReviewPackCard` - Shows latest pack status + metadata - Actions: Generate, Download (conditional) **Run**: `vendor/bin/sail artisan test --compact --filter=ReviewPackResource` --- ### Phase 5: Prune Command + Schedule **Goal:** Retention automation. 1. **Command**: `tenantpilot:review-pack:prune` - Marks expired, deletes files, optional hard-delete - `--hard-delete` flag for DB row removal after grace period 2. **Schedule**: Wire in `routes/console.php` - `daily()` + `withoutOverlapping()` 3. **AlertRule cleanup**: Remove `sla_due` from dropdown options **Run**: `vendor/bin/sail artisan test --compact --filter="ReviewPackPrune|AlertRule"` --- ### Phase 6: Integration Tests **Goal:** End-to-end coverage. 1. RBAC enforcement tests (404 for non-members, 403 for insufficient caps) 2. Fingerprint dedupe test (duplicate generation → reuse) 3. Active-run dedupe test (concurrent generation → rejection) 4. Prune test (expired packs → status update + file deletion) 5. Download test (signed URL → file stream with correct headers) 6. Empty state + widget display tests **Run**: `vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/` --- ## File Inventory (New Files) ``` database/migrations/XXXX_create_review_packs_table.php app/Models/ReviewPack.php database/factories/ReviewPackFactory.php app/Support/ReviewPackStatus.php app/Support/Badges/Mappers/ReviewPackStatusBadge.php app/Services/ReviewPackService.php app/Jobs/GenerateReviewPackJob.php app/Notifications/ReviewPackStatusNotification.php app/Http/Controllers/ReviewPackDownloadController.php app/Filament/Resources/ReviewPackResource.php app/Filament/Resources/ReviewPackResource/Pages/ListReviewPacks.php app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php app/Filament/Widgets/TenantReviewPackCard.php app/Console/Commands/PruneReviewPacksCommand.php tests/Feature/ReviewPack/ReviewPackGenerationTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackRbacTest.php tests/Feature/ReviewPack/ReviewPackPruneTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/ReviewPack/ReviewPackWidgetTest.php ``` ## Modified Files ``` app/Support/OperationRunType.php — add ReviewPackGenerate case app/Support/Auth/Capabilities.php — add REVIEW_PACK_VIEW, REVIEW_PACK_MANAGE app/Support/Badges/BadgeDomain.php — add ReviewPackStatus case config/filesystems.php — add exports disk config/tenantpilot.php — add review_pack section routes/web.php — add download route routes/console.php — add prune schedule entry app/Filament/Resources/AlertRuleResource.php — hide sla_due option ```