feat: resend verification emails to unverified leads (#287)

## Summary

- Adds `leads:resend-verification-emails` artisan command that
dispatches `VerifyUserLeadEmailNotification` to all leads where
`email_verified_at IS NULL`
- Supports `--dry-run` flag to preview the count without sending
- Follows the same pattern as `leads:retry-failed-jobs` (progress bar,
summary table)

## Test plan

- [ ] `--dry-run` shows correct count without dispatching
- [ ] Command dispatches exactly one notification per unverified lead
- [ ] Verified leads are skipped
- [ ] Early exit when no unverified leads exist
This commit is contained in:
Víctor Falcón 2026-04-15 09:13:27 +01:00 committed by GitHub
parent f408dbe4c8
commit 5b78509588
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 98 additions and 0 deletions

View File

@ -0,0 +1,57 @@
<?php
namespace App\Console\Commands;
use App\Models\UserLead;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
#[Signature('leads:resend-verification-emails {--dry-run : Show what would happen without dispatching emails}')]
#[Description('Resend verification emails to leads that have not yet verified their email address')]
class ResendLeadVerificationEmailsCommand extends Command
{
public function handle(): int
{
$isDryRun = $this->option('dry-run');
if ($isDryRun) {
$this->warn('DRY RUN — no emails will be dispatched.');
$this->newLine();
}
$leads = UserLead::query()->whereNull('email_verified_at')->get();
if ($leads->isEmpty()) {
$this->info('No unverified leads found.');
return self::SUCCESS;
}
$dispatched = 0;
$progressBar = $this->output->createProgressBar($leads->count());
$progressBar->start();
foreach ($leads as $lead) {
if (! $isDryRun) {
$lead->sendEmailVerificationNotification();
}
$dispatched++;
$progressBar->advance();
}
$progressBar->finish();
$this->newLine(2);
$this->table(
['Action', 'Count'],
[
['dispatched'.($isDryRun ? ' (dry run)' : ''), $dispatched],
]
);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,41 @@
<?php
use App\Models\UserLead;
use App\Notifications\VerifyUserLeadEmailNotification;
use Illuminate\Support\Facades\Notification;
it('dispatches verification emails only to unverified leads', function () {
Notification::fake();
$unverified = UserLead::factory()->count(3)->create(['email_verified_at' => null]);
UserLead::factory()->create(['email_verified_at' => now()]);
$this->artisan('leads:resend-verification-emails')
->assertSuccessful();
Notification::assertSentTo($unverified, VerifyUserLeadEmailNotification::class);
Notification::assertCount(3);
});
it('does not dispatch emails in dry-run mode', function () {
Notification::fake();
UserLead::factory()->count(2)->create(['email_verified_at' => null]);
$this->artisan('leads:resend-verification-emails', ['--dry-run' => true])
->assertSuccessful();
Notification::assertNothingSent();
});
it('exits early when there are no unverified leads', function () {
Notification::fake();
UserLead::factory()->create(['email_verified_at' => now()]);
$this->artisan('leads:resend-verification-emails')
->expectsOutput('No unverified leads found.')
->assertSuccessful();
Notification::assertNothingSent();
});