Invite leads by email (#377)
## Summary - add --email option to leads:send-invitations - target one pending queued lead by email - cover targeted invite and missing pending lead cases ## Tests - vendor/bin/pint --dirty --format agent - php artisan test --compact tests/Feature/Commands/SendUserLeadInvitationsTest.php
This commit is contained in:
parent
3526e5fd12
commit
a20eeff378
|
|
@ -15,6 +15,7 @@ class SendUserLeadInvitations extends Command
|
|||
{
|
||||
protected $signature = 'leads:send-invitations
|
||||
{--limit=50 : Maximum number of leads to invite in this batch}
|
||||
{--email= : Invite a specific pending lead by email address}
|
||||
{--cohort= : Restrict to a single cohort (founder, founder_referrer, early_bird, waitlist)}
|
||||
{--dry-run : Show what would happen without sending or creating Stripe codes}
|
||||
{--force : Skip confirmation prompt}';
|
||||
|
|
@ -31,9 +32,10 @@ class SendUserLeadInvitations extends Command
|
|||
}
|
||||
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$emailFilter = $this->resolveEmailFilter();
|
||||
$cohortFilter = $this->resolveCohortFilter();
|
||||
|
||||
if ($cohortFilter === false) {
|
||||
if ($emailFilter === false || $cohortFilter === false) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
|
|
@ -41,11 +43,20 @@ class SendUserLeadInvitations extends Command
|
|||
->whereNotNull('position')
|
||||
->where('position', '>', 0)
|
||||
->whereNull('invitation_sent_at')
|
||||
->orderBy('position')
|
||||
->limit($limit * 5) // over-fetch when filtering by cohort
|
||||
->when(
|
||||
$emailFilter !== null,
|
||||
fn ($query) => $query->where('email', $emailFilter),
|
||||
fn ($query) => $query->orderBy('position')->limit($limit * 5), // over-fetch when filtering by cohort
|
||||
)
|
||||
->get();
|
||||
|
||||
if ($candidates->isEmpty()) {
|
||||
if ($emailFilter !== null) {
|
||||
$this->error("No pending lead found for {$emailFilter}.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('No pending leads found.');
|
||||
|
||||
return self::SUCCESS;
|
||||
|
|
@ -136,6 +147,25 @@ class SendUserLeadInvitations extends Command
|
|||
return $failed === 0 ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
|
||||
private function resolveEmailFilter(): string|false|null
|
||||
{
|
||||
$email = $this->option('email');
|
||||
|
||||
if ($email === null || $email === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$email = strtolower(trim((string) $email));
|
||||
|
||||
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->error("Invalid email `{$email}`.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
private function resolveCohortFilter(): LeadCohort|false|null
|
||||
{
|
||||
$cohort = $this->option('cohort');
|
||||
|
|
|
|||
|
|
@ -80,3 +80,30 @@ it('records the cohort on each invited lead', function (): void {
|
|||
expect($cohorts[9])->toBe(LeadCohort::Founder);
|
||||
expect($cohorts[10])->toBe(LeadCohort::EarlyBird);
|
||||
});
|
||||
|
||||
it('sends an invitation to a specific pending lead by email', function (): void {
|
||||
UserLead::factory()->count(12)->state(new Sequence(
|
||||
...array_map(fn (int $i) => ['position' => $i, 'email_verified_at' => now()], range(1, 12)),
|
||||
))->create();
|
||||
|
||||
$target = UserLead::factory()->ranked(42)->create(['email' => 'target@example.com']);
|
||||
|
||||
$this->artisan('leads:send-invitations', ['--email' => 'target@example.com', '--force' => true])
|
||||
->assertSuccessful();
|
||||
|
||||
Mail::assertQueued(UserLeadInvitation::class, 1);
|
||||
Mail::assertQueued(UserLeadInvitation::class, fn (UserLeadInvitation $mail): bool => $mail->hasTo('target@example.com'));
|
||||
|
||||
expect($target->refresh()->invitation_sent_at)->not->toBeNull()
|
||||
->and(UserLead::query()->whereNotNull('invitation_sent_at')->pluck('email')->all())->toBe(['target@example.com']);
|
||||
});
|
||||
|
||||
it('fails when the requested email is not a pending lead', function (): void {
|
||||
UserLead::factory()->ranked(1)->create(['email' => 'already@example.com', 'invitation_sent_at' => now()]);
|
||||
|
||||
$this->artisan('leads:send-invitations', ['--email' => 'already@example.com', '--force' => true])
|
||||
->expectsOutput('No pending lead found for already@example.com.')
|
||||
->assertFailed();
|
||||
|
||||
Mail::assertNothingQueued();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue