Story 3-4: Bulk client notification scheduling — BulkNotificationController, BulkActionBar component, checkbox selection on declarations index. Story 3-5: Email notification enhancement — observer-driven email on en_attente_client, cache invalidation on ferme, workspace branding on all email templates, 11 feature tests. Code review fixes: - Move bulk-notify route above resource wildcard to prevent shadowing - Add static $suppressEmail flag to prevent observer double-sending when DeclarationMessageController already sends the email - Fix canBulkNotify logic (was granting workers access) - Add WorkspaceUserRole check to BulkNotifyRequest::authorize() - Replace firstOrCreate with explicit invitation lookup that syncs client email and handles used/expired invitations correctly - Watch declarations.data instead of current_page to clear selection on filter/sort changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14 KiB
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)
-
Given a user (Owner/Manager/Worker with assigned declarations) is viewing the declarations list When they select multiple declarations with status
en_attente_clientusing row checkboxes Then a BulkActionBar appears showing "[N] selected" and a "Notify Clients" button -
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
-
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"
-
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
-
Given a Worker user selects declarations Then they can only bulk-notify clients for their own assigned declarations (enforced server-side via
scopeForUser) -
Given the existing email template
DeclarationFileRequestMailexists Then it is reused for the notification content (no new mailable needed)
Tasks / Subtasks
-
Task 1: Backend - BulkNotificationController (AC: 1,2,3,4,5)
- 1.1 Create
app/Http/Controllers/BulkNotificationController.phpwithstore()method - 1.2 Use
HasWorkspaceScopetrait, validate request input (array of declaration IDs) - 1.3 Load declarations with workspace scoping +
scopeForUserfor Workers - 1.4 Filter to only
en_attente_clientstatus declarations server-side - 1.5 For each declaration: find/create
DeclarationInvitation, dispatchDeclarationFileRequestMailvia queue - 1.6 Log bulk operation via
activity()->performedOn($workspace)->causedBy($user)->withProperties(['count' => $count, 'declaration_ids' => $ids])->log('bulk_client_notification') - 1.7 Return immediately with flash success message (non-blocking)
- 1.1 Create
-
Task 2: Backend - Route registration (AC: 3)
- 2.1 Add
POST /declarations/bulk-notifyroute inweb.phpwiththrottle:5,1middleware - 2.2 Name:
declarations.bulk-notify
- 2.1 Add
-
Task 3: Frontend - Row selection on declarations Index (AC: 1)
- 3.1 Add checkbox column to declarations table in
resources/js/Pages/declarations/Index.vue - 3.2 Track selected declaration IDs in reactive state
- 3.3 Only allow selection of rows with status
en_attente_client - 3.4 Add select-all checkbox in header (filtered to eligible rows only)
- 3.1 Add checkbox column to declarations table in
-
Task 4: Frontend - BulkActionBar component (AC: 1,2)
- 4.1 Create
resources/js/components/declarations/BulkActionBar.vue - 4.2 Fixed bottom bar showing "[N] selected" count and "Notify Clients" button
- 4.3 Only visible when
selectedIds.length > 0 - 4.4 Mobile: render as bottom sheet (responsive)
- 4.1 Create
-
Task 5: Frontend - Confirmation dialog and submission (AC: 2,3)
- 5.1 Use shadcn-vue
Dialogfor confirmation (AlertDialog not installed, Dialog used instead) - 5.2 Show client count and "Send Notifications" primary button
- 5.3 POST to
declarations.bulk-notifyroute via Inertiarouter.post() - 5.4 Disable button during processing with spinner
- 5.5 Show success toast on completion, clear selection state
- 5.1 Use shadcn-vue
-
Task 6: Backend - Pass bulk-notify URL and selection eligibility to frontend (AC: 1,5)
- 6.1 Add
bulkNotifyUrlprop fromDeclarationController::index()viaroute('declarations.bulk-notify') - 6.2 Add
canBulkNotifyboolean prop (Owner/Manager always true, Worker true if has assignments)
- 6.1 Add
-
Task 7: Tests (AC: all)
- 7.1 Feature test: successful bulk notification dispatch for Owner/Manager
- 7.2 Feature test: Worker can only bulk-notify own assigned declarations
- 7.3 Feature test: rejects declarations not in
en_attente_clientstatus - 7.4 Feature test: workspace boundary enforcement (abort 404)
- 7.5 Feature test: activity log records bulk operation
- 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 accessingWorkspaceUserpivot. 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)
// 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. UseTransitionfor 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 })-- NOTuseForm()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 RefreshDatabaseauto-applied via Pest.php- Test file:
tests/Feature/Notifications/BulkNotificationTest.php - Assert
Mail::fake()+Mail::assertQueued(DeclarationFileRequestMail::class, $count) - Assert
Activitymodel 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
DeclarationFileRequestMailis alreadyQueueablewithSerializesModels - 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)