From ebbc1795b9c7b5b24350079e9fb348a4fee32df2 Mon Sep 17 00:00:00 2001 From: Peter Giles <8978655+petertgiles@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:32:10 -0500 Subject: [PATCH] [Feature] Ad hoc email command (#12325) * basic filtering * fix json querying * extra validation * new notification * template handling * linting * fix function name typo * switch to InvalidArgumentException * use word queue in messages --- .../Commands/SendNotificationsAdHocEmail.php | 148 ++++++++++++++++++ api/app/Notifications/AdHocEmail.php | 56 +++++++ 2 files changed, 204 insertions(+) create mode 100644 api/app/Console/Commands/SendNotificationsAdHocEmail.php create mode 100644 api/app/Notifications/AdHocEmail.php diff --git a/api/app/Console/Commands/SendNotificationsAdHocEmail.php b/api/app/Console/Commands/SendNotificationsAdHocEmail.php new file mode 100644 index 00000000000..c404d47c6b3 --- /dev/null +++ b/api/app/Console/Commands/SendNotificationsAdHocEmail.php @@ -0,0 +1,148 @@ +getUsersToSendNotificationTo(); + $userCount = $users->count(); + + if ($this->confirm('Do you wish to send notifications to '.$userCount.' users?')) { + $progressBar = $this->output->createProgressBar($userCount); + $notification = new AdHocEmail($this->argument('templateIdEn'), $this->argument('templateIdFr')); + + $users->chunk(200, function (Collection $chunkOfUsers) use (&$successCount, &$failureCount, $progressBar, $notification) { + /** @var \App\Models\User $user */ + foreach ($chunkOfUsers as $user) { + try { + $user->notify($notification); + $successCount++; + } catch (\Throwable $e) { + $this->error("Failed to queue notification for user $user->id: ".$e->getMessage()); + $failureCount++; + } finally { + $progressBar->advance(); + } + } + }); + $this->newLine(); + + $this->info("Notifications queued. Success: $successCount Failure: $failureCount"); + if ($failureCount > 0) { + return Command::FAILURE; + } else { + return Command::SUCCESS; + } + } else { + $this->info('Notification sending cancelled'); + + return Command::SUCCESS; + } + } + + private function getUsersToSendNotificationTo(): Builder + { + $emailAddresses = $this->option('emailAddress'); + $notificationFamilies = $this->option('notificationFamily'); + $notifyAllUsers = $this->option('notifyAllUsers'); + + $options = $this->options(); + + // How many types of options were used? + $optionTypesCount = + (count($emailAddresses) > 0 ? 1 : 0) + + (count($notificationFamilies) > 0 ? 1 : 0) + + ($notifyAllUsers ? 1 : 0); + + if ($optionTypesCount != 1) { + throw new \InvalidArgumentException('Must filter users using exactly one of the option types'); + } + + if (count($emailAddresses) > 0) { + return $this->builderFromEmailAddresses($emailAddresses); + } + + if (count($notificationFamilies) > 0) { + return $this->builderFromNotificationFamilies($notificationFamilies); + } + + if ($notifyAllUsers) { + return User::query(); + } + + throw new \InvalidArgumentException('Unexpected function end point for options: '.json_encode($options)); + } + + private function builderFromEmailAddresses(array $requestedEmailAddresses): Builder + { + $builder = User::whereIn('email', $requestedEmailAddresses); + + // Check for missing email addresses + $dbEmailAddresses = $builder->pluck('email')->toArray(); + $missingEmailAddresses = array_diff($requestedEmailAddresses, $dbEmailAddresses); + if (count($missingEmailAddresses) > 0) { + $this->alert('The following email addresses were not found:'); + foreach ($missingEmailAddresses as $missingEmailAddress) { + $this->warn($missingEmailAddress); + } + } + + return $builder; + } + + private function builderFromNotificationFamilies(array $notificationFamilies): Builder + { + // Check for bad notification families + $allNotificationFamilies = array_column(NotificationFamily::cases(), 'name'); + foreach ($notificationFamilies as $notificationFamily) { + if (! in_array($notificationFamily, $allNotificationFamilies)) { + throw new \InvalidArgumentException('Invalid notification family: '.$notificationFamily); + } + } + + $builder = User::whereJsonContains('enabled_email_notifications', $notificationFamilies[0]); + for ($i = 1; $i < count($notificationFamilies); $i++) { + $builder->orWhereJsonContains('enabled_email_notifications', $notificationFamilies[$i]); + } + + return $builder; + } +} diff --git a/api/app/Notifications/AdHocEmail.php b/api/app/Notifications/AdHocEmail.php new file mode 100644 index 00000000000..718bdf1d59a --- /dev/null +++ b/api/app/Notifications/AdHocEmail.php @@ -0,0 +1,56 @@ + + */ + public function via(): array + { + return [GcNotifyEmailChannel::class]; + } + + /** + * Get the GC Notify representation of the notification. + */ + public function toGcNotifyEmail(User $notifiable): GcNotifyEmailMessage + { + $locale = $this->locale ?? $notifiable->preferredLocale(); + $templateId = match ($locale) { + Language::EN->value => $this->templateIdEn, + Language::FR->value => $this->templateIdFr, + default => throw new \InvalidArgumentException("Unsupported locale: $locale"), + }; + + $message = new GcNotifyEmailMessage( + $templateId, + $notifiable->email, + [ + 'first_name' => $notifiable->first_name, + 'last_name' => $notifiable->last_name, + ] + ); + + return $message; + } +}