option('limit'); if ($limit < 1) { $this->error('Limit must be a positive integer.'); return self::FAILURE; } $dryRun = (bool) $this->option('dry-run'); $emailFilter = $this->resolveEmailFilter(); $cohortFilter = $this->resolveCohortFilter(); if ($emailFilter === false || $cohortFilter === false) { return self::FAILURE; } $candidates = UserLead::query() ->whereNotNull('position') ->where('position', '>', 0) ->whereNull('invitation_sent_at') ->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; } $plan = []; foreach ($candidates as $lead) { $cohort = $resolver->resolve($lead); if ($cohort === null) { continue; } if ($cohortFilter !== null && $cohort !== $cohortFilter) { continue; } $plan[] = [$lead, $cohort]; if (count($plan) >= $limit) { break; } } if ($plan === []) { $this->info('No leads matched the requested filters.'); return self::SUCCESS; } $this->table( ['#', 'Position', 'Email', 'Cohort'], array_map( fn (array $row, int $i): array => [ $i + 1, $row[0]->position, $row[0]->email, $row[1]->value, ], $plan, array_keys($plan), ), ); if ($dryRun) { $this->info('[dry-run] No emails sent.'); return self::SUCCESS; } if (! $this->option('force')) { if (! $this->confirm('Send these invitation emails?', true)) { $this->info('Cancelled.'); return self::SUCCESS; } } $sent = 0; $failed = 0; $progressBar = $this->output->createProgressBar(count($plan)); $progressBar->start(); foreach ($plan as [$lead, $cohort]) { try { /** @var UserLead $lead */ /** @var LeadCohort $cohort */ $lead->cohort = $cohort; $allocator->ensureCodes($lead, $cohort); Mail::to($lead->email)->send(new UserLeadInvitation($lead->fresh(), $cohort)); $lead->forceFill(['invitation_sent_at' => now()])->save(); $sent++; } catch (Throwable $exception) { $failed++; $this->error("Failed for {$lead->email}: {$exception->getMessage()}"); report($exception); } $progressBar->advance(); } $progressBar->finish(); $this->newLine(); $this->info("Queued {$sent} invitation email(s)".($failed > 0 ? " ({$failed} failed)" : '').'.'); 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'); if ($cohort === null || $cohort === '') { return null; } $resolved = LeadCohort::tryFrom((string) $cohort); if ($resolved === null) { $this->error("Unknown cohort `{$cohort}`. Valid values: founder, founder_referrer, early_bird, waitlist."); return false; } return $resolved; } }