option('stats')) { $this->displayStats(); return self::SUCCESS; } $limit = (int) $this->option('limit'); if ($limit < 1) { $this->error('Limit must be a positive integer.'); return self::FAILURE; } $minDaysSinceInvite = max(0, (int) $this->option('min-days-since-invite')); $emailFilter = $this->resolveEmailFilter(); if ($emailFilter === false) { return self::FAILURE; } $leads = $this->pendingReInvitationQuery($minDaysSinceInvite, (bool) $this->option('again')) ->when( $emailFilter !== null, fn (Builder $query) => $query->where('email', $emailFilter), fn (Builder $query) => $query->orderBy('invitation_sent_at')->limit($limit), ) ->get(); if ($leads->isEmpty()) { if ($emailFilter !== null) { $this->error("No invited lead pending re-invitation found for {$emailFilter}."); return self::FAILURE; } $this->info('No invited leads pending re-invitation found.'); return self::SUCCESS; } $this->table( ['#', 'Email', 'Invited at', 'Re-invited at', 'Re-invites'], $leads->values()->map(fn (UserLead $lead, int $index): array => [ $index + 1, $lead->email, $lead->invitation_sent_at?->toDateTimeString(), $lead->re_invitation_sent_at?->toDateTimeString() ?? '-', $lead->re_invitation_count ?? 0, ])->all(), ); if ((bool) $this->option('dry-run')) { $this->info('[dry-run] No re-invitation emails sent.'); return self::SUCCESS; } if (! $this->option('force')) { if (! $this->confirm('Send these re-invitation emails?', true)) { $this->info('Cancelled.'); return self::SUCCESS; } } $sent = 0; $failed = 0; $progressBar = $this->output->createProgressBar($leads->count()); $progressBar->start(); foreach ($leads as $lead) { try { Mail::to($lead->email)->send(new UserLeadReInvitation($lead)); $lead->forceFill([ 're_invitation_sent_at' => now(), 're_invitation_count' => ((int) $lead->re_invitation_count) + 1, ])->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} re-invitation email(s)".($failed > 0 ? " ({$failed} failed)" : '').'.'); $this->displayStats(); return $failed === 0 ? self::SUCCESS : self::FAILURE; } /** @return Builder */ private function pendingReInvitationQuery(int $minDaysSinceInvite, bool $includeAlreadyReInvited): Builder { return UserLead::query() ->whereNotNull('invitation_sent_at') ->whereDoesntHave('signedUpUser') ->when(! $includeAlreadyReInvited, fn (Builder $query) => $query->whereNull('re_invitation_sent_at')) ->when( $minDaysSinceInvite > 0, fn (Builder $query) => $query->where('invitation_sent_at', '<=', now()->subDays($minDaysSinceInvite)), ); } private function displayStats(): void { $reInvited = UserLead::query() ->whereNotNull('re_invitation_sent_at') ->count(); $signedUpAfterReInvite = UserLead::query() ->whereNotNull('re_invitation_sent_at') ->whereHas('signedUpUser', fn (Builder $query) => $query->whereColumn('users.created_at', '>=', 'user_leads.re_invitation_sent_at')) ->count(); $rate = $reInvited > 0 ? round(($signedUpAfterReInvite / $reInvited) * 100, 2) : 0.0; $this->table(['Metric', 'Value'], [ ['Re-invited leads', $reInvited], ['Re-invited leads signed up', $signedUpAfterReInvite], ['Success rate', $rate.'%'], ]); } 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; } }