Files
L-Ami-Fiduciaire/tests/Feature/Notifications/EmailNotificationTest.php
Saad Zoubir 1d4f3bcd0f feat: add bulk client notifications and email enhancements with review fixes (Stories 3.4 & 3.5)
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>
2026-03-26 14:31:36 +01:00

196 lines
6.6 KiB
PHP

<?php
use App\Enums\DeclarationStatus;
use App\Mail\DeclarationFileRequestMail;
use App\Mail\NudgeNotificationMail;
use App\Models\Declaration;
use App\Models\User;
use App\Models\Workspace;
use App\Notifications\DocumentUploadedNotification;
use App\Notifications\NudgeNotification;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
function setupEmailNotificationTest(): array
{
$user = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($user->id, ['role' => 'owner', 'permissions' => []]);
$declaration = Declaration::factory()->forWorkspace($workspace)->create([
'status' => DeclarationStatus::EnCours,
'assigned_to' => $user->id,
]);
session(['current_workspace_id' => $workspace->id]);
return [$user, $workspace, $declaration];
}
// ── AC3: Status change to en_attente_client triggers client email ──
test('status change to en_attente_client queues client email', function () {
Mail::fake();
[$user, $workspace, $declaration] = setupEmailNotificationTest();
$declaration->update(['status' => DeclarationStatus::EnAttenteClient]);
Mail::assertQueued(DeclarationFileRequestMail::class, function ($mail) use ($declaration) {
return $mail->declaration->id === $declaration->id;
});
});
test('status change to en_attente_client creates declaration invitation', function () {
Mail::fake();
[$user, $workspace, $declaration] = setupEmailNotificationTest();
$declaration->update(['status' => DeclarationStatus::EnAttenteClient]);
expect($declaration->invitations()->count())->toBe(1);
expect($declaration->invitations()->first()->email)->toBe($declaration->client->contact_email);
});
test('status change to en_attente_client skips email when client has no email', function () {
Mail::fake();
[$user, $workspace, $declaration] = setupEmailNotificationTest();
$declaration->client->update(['contact_email' => null]);
$declaration->update(['status' => DeclarationStatus::EnAttenteClient]);
Mail::assertNothingQueued();
});
// ── AC4: Status change to ferme invalidates dashboard cache ──
test('status change to ferme invalidates dashboard cache', function () {
Mail::fake();
$user = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($user->id, ['role' => 'owner', 'permissions' => []]);
$declaration = Declaration::factory()->forWorkspace($workspace)->create([
'status' => DeclarationStatus::Termine,
'assigned_to' => $user->id,
]);
$cacheKey = "dashboard:{$workspace->id}:{$user->id}";
Cache::put($cacheKey, 'cached-data', 300);
expect(Cache::has($cacheKey))->toBeTrue();
$declaration->update(['status' => DeclarationStatus::Ferme]);
expect(Cache::has($cacheKey))->toBeFalse();
});
// ── AC6: Status change to en_cours does NOT trigger any email ──
test('status change to en_cours does not trigger any email', function () {
Mail::fake();
$user = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($user->id, ['role' => 'owner', 'permissions' => []]);
$declaration = Declaration::factory()->forWorkspace($workspace)->create([
'status' => DeclarationStatus::Created,
'assigned_to' => $user->id,
]);
$declaration->update(['status' => DeclarationStatus::EnCours]);
Mail::assertNothingQueued();
});
// ── AC2: DocumentUploadedNotification has no mail channel ──
test('document uploaded notification has no mail channel', function () {
$declaration = Declaration::factory()->create();
$notification = new DocumentUploadedNotification($declaration, 1);
$channels = $notification->via(new stdClass);
expect($channels)->toBe(['database']);
expect($channels)->not->toContain('mail');
});
// ── AC1: Nudge email contains required fields ──
test('nudge email contains required fields', function () {
$user = User::factory()->create();
$workspace = Workspace::factory()->create(['name' => 'Cabinet Test']);
$workspace->users()->attach($user->id, ['role' => 'owner', 'permissions' => []]);
$declaration = Declaration::factory()->forWorkspace($workspace)->create([
'assigned_to' => $user->id,
]);
$mail = new NudgeNotificationMail($declaration, $user);
$rendered = $mail->render();
expect($rendered)->toContain($user->name);
expect($rendered)->toContain('Voir la déclaration');
expect($rendered)->toContain('Cabinet Test');
});
// ── AC1: Nudge notification sends via database and mail channels ──
test('nudge notification sends via database and mail channels', function () {
$declaration = Declaration::factory()->create();
$sender = User::factory()->create();
$notification = new NudgeNotification($declaration, $sender);
$channels = $notification->via(new stdClass);
expect($channels)->toBe(['database', 'mail']);
});
// ── AC6: Workspace scoping on notification dispatches ──
test('status change email is scoped to the correct workspace client', function () {
Mail::fake();
[$user, $workspace, $declaration] = setupEmailNotificationTest();
$otherWorkspace = Workspace::factory()->create();
$otherDeclaration = Declaration::factory()->forWorkspace($otherWorkspace)->create([
'status' => DeclarationStatus::EnCours,
]);
// Only our declaration should trigger email
$declaration->update(['status' => DeclarationStatus::EnAttenteClient]);
$otherDeclaration->update(['status' => DeclarationStatus::EnAttenteClient]);
Mail::assertQueued(DeclarationFileRequestMail::class, 2);
// Each email goes to the correct client
Mail::assertQueued(DeclarationFileRequestMail::class, function ($mail) use ($declaration) {
return $mail->declaration->id === $declaration->id;
});
});
// ── AC5: Failed email retries — verify $tries and $backoff properties ──
test('nudge notification has retry configuration', function () {
$declaration = Declaration::factory()->create();
$sender = User::factory()->create();
$notification = new NudgeNotification($declaration, $sender);
expect($notification->tries)->toBe(3);
expect($notification->backoff)->toBe(60);
});
test('declaration overdue notification has retry configuration', function () {
$declaration = Declaration::factory()->create();
$notification = new \App\Notifications\DeclarationOverdueNotification($declaration);
expect($notification->tries)->toBe(3);
expect($notification->backoff)->toBe(60);
});