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); });