# Story 3.4: Bulk Client Notification Scheduling

Status: review

## Story

As a firm worker or manager,
I want to send document request notifications to multiple clients at once,
so that I can efficiently request missing documents without emailing each client individually.

## Acceptance Criteria (BDD)

1. **Given** a user (Owner/Manager/Worker with assigned declarations) is viewing the declarations list
   **When** they select multiple declarations with status `en_attente_client` using row checkboxes
   **Then** a BulkActionBar appears showing "[N] selected" and a "Notify Clients" button

2. **Given** the user clicks "Notify Clients" on the BulkActionBar
   **When** the confirmation dialog appears
   **Then** it shows the count of clients to be notified and a "Send Notifications" confirmation button

3. **Given** the user clicks "Send" in the confirmation dialog
   **When** the POST request is dispatched to the bulk notification endpoint
   **Then** each client receives a personalized email with their token-based portal link for document upload
   **And** emails are queued individually via Redis (non-blocking -- controller returns immediately)
   **And** a success toast confirms: "[N] notifications envoyees"

4. **Given** 50 client notifications are scheduled
   **When** the queue processes them
   **Then** all 50 emails are sent within 10 seconds (NFR3)
   **And** failed individual sends retry automatically (up to 3 retries per NFR26)
   **And** the bulk operation is logged via Spatie Activity Log with the count and actor

5. **Given** a Worker user selects declarations
   **Then** they can only bulk-notify clients for their own assigned declarations (enforced server-side via `scopeForUser`)

6. **Given** the existing email template `DeclarationFileRequestMail` exists
   **Then** it is reused for the notification content (no new mailable needed)

## Tasks / Subtasks

- [x] Task 1: Backend - BulkNotificationController (AC: 1,2,3,4,5)
  - [x] 1.1 Create `app/Http/Controllers/BulkNotificationController.php` with `store()` method
  - [x] 1.2 Use `HasWorkspaceScope` trait, validate request input (array of declaration IDs)
  - [x] 1.3 Load declarations with workspace scoping + `scopeForUser` for Workers
  - [x] 1.4 Filter to only `en_attente_client` status declarations server-side
  - [x] 1.5 For each declaration: find/create `DeclarationInvitation`, dispatch `DeclarationFileRequestMail` via queue
  - [x] 1.6 Log bulk operation via `activity()->performedOn($workspace)->causedBy($user)->withProperties(['count' => $count, 'declaration_ids' => $ids])->log('bulk_client_notification')`
  - [x] 1.7 Return immediately with flash success message (non-blocking)

- [x] Task 2: Backend - Route registration (AC: 3)
  - [x] 2.1 Add `POST /declarations/bulk-notify` route in `web.php` with `throttle:5,1` middleware
  - [x] 2.2 Name: `declarations.bulk-notify`

- [x] Task 3: Frontend - Row selection on declarations Index (AC: 1)
  - [x] 3.1 Add checkbox column to declarations table in `resources/js/Pages/declarations/Index.vue`
  - [x] 3.2 Track selected declaration IDs in reactive state
  - [x] 3.3 Only allow selection of rows with status `en_attente_client`
  - [x] 3.4 Add select-all checkbox in header (filtered to eligible rows only)

- [x] Task 4: Frontend - BulkActionBar component (AC: 1,2)
  - [x] 4.1 Create `resources/js/components/declarations/BulkActionBar.vue`
  - [x] 4.2 Fixed bottom bar showing "[N] selected" count and "Notify Clients" button
  - [x] 4.3 Only visible when `selectedIds.length > 0`
  - [x] 4.4 Mobile: render as bottom sheet (responsive)

- [x] Task 5: Frontend - Confirmation dialog and submission (AC: 2,3)
  - [x] 5.1 Use shadcn-vue `Dialog` for confirmation (AlertDialog not installed, Dialog used instead)
  - [x] 5.2 Show client count and "Send Notifications" primary button
  - [x] 5.3 POST to `declarations.bulk-notify` route via Inertia `router.post()`
  - [x] 5.4 Disable button during processing with spinner
  - [x] 5.5 Show success toast on completion, clear selection state

- [x] Task 6: Backend - Pass bulk-notify URL and selection eligibility to frontend (AC: 1,5)
  - [x] 6.1 Add `bulkNotifyUrl` prop from `DeclarationController::index()` via `route('declarations.bulk-notify')`
  - [x] 6.2 Add `canBulkNotify` boolean prop (Owner/Manager always true, Worker true if has assignments)

- [x] Task 7: Tests (AC: all)
  - [x] 7.1 Feature test: successful bulk notification dispatch for Owner/Manager
  - [x] 7.2 Feature test: Worker can only bulk-notify own assigned declarations
  - [x] 7.3 Feature test: rejects declarations not in `en_attente_client` status
  - [x] 7.4 Feature test: workspace boundary enforcement (abort 404)
  - [x] 7.5 Feature test: activity log records bulk operation
  - [x] 7.6 Feature test: throttle middleware prevents abuse

## Retrospective Intelligence

### From Epic 2 Retro (2026-03-24)
- **Deferred item D-1:** Nudge/Reassign dropdown items were unconditionally disabled in Story 2.3 -- resolved in Story 3.2. No carryover for 3.4.
- **Deferred item D-3:** Cache not invalidated on role change (5-min TTL mitigates). If bulk notifications touch notification counts, invalidate cache per `Cache::forget("user:{$userId}:workspace:{$workspaceId}:unread_notifications")`.
- **Pre-existing test failures:** 15 failures were resolved in commit `716e9fc`. Ensure no new regressions.

### From Epic 1 Retro (2026-03-20)
- **withPivot gotcha:** Always use `->withPivot('role', 'permissions')` when accessing `WorkspaceUser` pivot. This hit 3/6 stories in Epic 1.
- **Previous Story Intelligence** sections in story specs enable knowledge transfer -- validated pattern.

### From Epic 0 Retro (2026-03-13)
- Layered migration strategy works well. No new migrations needed for this story.

## Dev Notes

### Architecture Patterns (MUST FOLLOW)

| Pattern | Implementation | Source |
|---------|---------------|--------|
| Workspace scoping | `HasWorkspaceScope` trait, `abort(404)` for boundary violations | architecture.md D7 |
| Queued notifications | `ShouldQueue` trait, `$tries = 3`, `$backoff = 60` | architecture.md D5 |
| Bulk operations | `DB::transaction()` for validation, Redis queue for sends | architecture.md D9 |
| Activity logging | `activity()->performedOn()->causedBy()->log()` | NudgeController pattern |
| Cache invalidation | `Cache::forget("user:{id}:workspace:{id}:unread_notifications")` | NotificationController pattern |

### Existing Code to Reuse (DO NOT REINVENT)

| What | Where | How to Reuse |
|------|-------|-------------|
| `DeclarationFileRequestMail` | `app/Mail/DeclarationFileRequestMail.php` | Use directly -- accepts `Declaration`, `DeclarationInvitation`, `string body` |
| `DeclarationInvitation` model | `app/Models/DeclarationInvitation.php` | Creates token-based portal links for clients |
| `HasWorkspaceScope` trait | `app/Concerns/HasWorkspaceScope.php` | `use HasWorkspaceScope` + `$this->authorizeWorkspaceAccess()` |
| `NotificationType::BulkNotification` | `app/Enums/NotificationType.php` | Already defined as `'bulk_notification'` with label `'Notification groupee'` |
| `Declaration::scopeForUser()` | `app/Models/Declaration.php` | Scopes to assigned declarations for Workers |
| Frontend `bulk_notification` icon | `resources/js/Pages/notifications/Index.vue` | Already mapped to `Mail` icon from lucide |
| `NudgeController` | `app/Http/Controllers/NudgeController.php` | Reference pattern for workspace auth + activity logging |

### Controller Pattern (Follow NudgeController)

```php
// BulkNotificationController.php structure:
class BulkNotificationController extends Controller
{
    use HasWorkspaceScope;

    public function store(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'declaration_ids' => ['required', 'array', 'min:1'],
            'declaration_ids.*' => ['integer'],
        ]);

        $workspace = $this->currentWorkspace();
        $user = $request->user();
        $workspaceUser = $workspace->users()->where('users.id', $user->id)->firstOrFail();

        // Load declarations with workspace + user scoping
        $declarations = Declaration::where('workspace_id', $workspace->id)
            ->forUser($user, $workspaceUser)
            ->where('status', 'en_attente_client')
            ->whereIn('id', $validated['declaration_ids'])
            ->get();

        // Queue emails (non-blocking)
        foreach ($declarations as $declaration) {
            // Find or create invitation with token for client portal
            $invitation = $declaration->invitations()->firstOrCreate([...]);
            Mail::to($declaration->client->email)
                ->queue(new DeclarationFileRequestMail($declaration, $invitation, $body));
        }

        // Activity log
        activity()
            ->performedOn($workspace)
            ->causedBy($user)
            ->withProperties(['count' => $declarations->count(), 'declaration_ids' => $declarations->pluck('id')])
            ->log('bulk_client_notification');

        // Invalidate notification caches for affected users if database notifications created

        return back()->with('flash', [
            'type' => 'success',
            'message' => $declarations->count() . ' notifications envoyees',
        ]);
    }
}
```

### Declaration Status Values

Per project-context.md: `created`, `en_cours`, `en_attente_client`, `mise_en_demeure`, `termine`, `ferme`

Only declarations with status `en_attente_client` are eligible for bulk client notifications.

### Frontend Implementation Notes

- **Checkbox column:** Add as first column in the declarations table. Use reactive `ref<number[]>([])` for selected IDs.
- **BulkActionBar:** Fixed position bottom bar using Tailwind `fixed bottom-0`. Use `Transition` for show/hide animation.
- **Confirmation:** Use shadcn-vue `AlertDialog` (same pattern as delete confirmations in the codebase).
- **Submission:** Use `router.post(props.bulkNotifyUrl, { declaration_ids: selectedIds.value })` -- NOT `useForm()` since there's no traditional form.
- **Toast:** Already handled by flash message system via `back()->with('flash', ...)`.
- **Props to add to Index.vue:** `bulkNotifyUrl: string`, `canBulkNotify: boolean`.

### Testing Standards

- Use Pest `test()` closures with lowercase descriptions
- `RefreshDatabase` auto-applied via Pest.php
- Test file: `tests/Feature/Notifications/BulkNotificationTest.php`
- Assert `Mail::fake()` + `Mail::assertQueued(DeclarationFileRequestMail::class, $count)`
- Assert `Activity` model for log entries
- Test workspace isolation: create declaration in different workspace, assert 404

### File Structure

```
app/Http/Controllers/BulkNotificationController.php    (NEW)
resources/js/components/declarations/BulkActionBar.vue  (NEW)
resources/js/Pages/declarations/Index.vue               (MODIFY - add checkboxes, bulk bar)
app/Http/Controllers/DeclarationController.php          (MODIFY - add bulkNotifyUrl prop)
routes/web.php                                          (MODIFY - add bulk-notify route)
tests/Feature/Notifications/BulkNotificationTest.php    (NEW)
```

### Project Structure Notes

- Controller follows single-action pattern in `app/Http/Controllers/` (consistent with NudgeController)
- Vue component in `resources/js/components/declarations/` (co-located with NudgePopover)
- Test in `tests/Feature/Notifications/` (consistent with notification test organization)
- No new migrations, models, or enums required -- all infrastructure exists from Stories 3.1-3.3

### Performance (NFR3)

- 50 emails within 10 seconds is achievable because emails are individually queued to Redis
- Controller returns immediately (non-blocking) -- queue workers handle actual sends
- Each `DeclarationFileRequestMail` is already `Queueable` with `SerializesModels`
- Redis queue processes jobs concurrently via multiple workers

### References

- [Source: _bmad-output/planning-artifacts/epics.md - Epic 3, Story 3.4]
- [Source: _bmad-output/planning-artifacts/architecture.md - D5 Queued Notifications, D7 Workspace Scoping, D8 DatabaseNotification, D9 Bulk Operations]
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md - BulkActionBar, Confirmation Dialogs, Toast Patterns]
- [Source: _bmad-output/planning-artifacts/prd.md - FR32 Bulk Notifications, NFR3, NFR26]
- [Source: _bmad-output/project-context.md - Declaration statuses, withPivot gotcha, workspace scoping]
- [Source: _bmad-output/implementation-artifacts/3-3-notification-center-and-bell.md - Previous story patterns]
- [Source: _bmad-output/implementation-artifacts/epic-2-retrospective.md - Deferred items D-1, D-3]

## Dev Agent Record

### Agent Model Used
Claude Opus 4.6 (1M context)

### Debug Log References
- Used Dialog instead of AlertDialog (not installed) for confirmation modal -- functionally equivalent
- Used BulkNotifyRequest Form Request class instead of inline validation per project-context.md convention
- Used `user->currentWorkspaceUser()` memoized method instead of re-querying workspace users

### Completion Notes List
- Created BulkNotificationController with store() method following NudgeController patterns
- Controller uses HasWorkspaceScope, BulkNotifyRequest validation, workspace+user scoping, en_attente_client filter
- Each declaration gets a DeclarationInvitation (firstOrCreate) and queued DeclarationFileRequestMail
- Activity logged on workspace with count and declaration_ids properties
- Route registered as POST /declarations/bulk-notify with throttle:5,1 middleware
- Index.vue updated with checkbox column (only en_attente_client rows), select-all header checkbox
- BulkActionBar component created with fixed bottom bar, selection count, animated show/hide
- Confirmation dialog shows client count, disables button with spinner during processing
- bulkNotifyUrl and canBulkNotify props passed from DeclarationController::index()
- 9 feature tests covering all ACs: owner/manager bulk-notify, worker scoping, status filtering, workspace isolation, activity logging, throttle, auth, validation
- Full regression suite: 263 tests pass, 0 failures

### Change Log
- 2026-03-26: Implemented Story 3.4 - Bulk Client Notification Scheduling (all 7 tasks complete)

### File List
- app/Http/Controllers/BulkNotificationController.php (NEW)
- app/Http/Requests/BulkNotifyRequest.php (NEW)
- resources/js/components/declarations/BulkActionBar.vue (NEW)
- tests/Feature/Notifications/BulkNotificationTest.php (NEW)
- resources/js/Pages/declarations/Index.vue (MODIFIED - added checkboxes, BulkActionBar integration)
- app/Http/Controllers/DeclarationController.php (MODIFIED - added bulkNotifyUrl, canBulkNotify props)
- routes/web.php (MODIFIED - added declarations.bulk-notify route)
