From e6cbba8c633d66907c0dc99142e75bbe5b6f9f7b Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 19 Jun 2025 20:23:43 +0200 Subject: [PATCH] WIP implement immediate and daily email functionality --- config/packages/cache_chill.yaml | 4 + .../ScheduleDailyNotificationEmailHandler.php | 70 ++++++++ .../SendDailyDigestHandler.php | 66 +++++++ .../SendImmediateNotificationEmailHandler.php | 68 +++++++ .../ScheduleDailyNotificationEmailMessage.php | 30 ++++ .../SendDailyDigestMessage.php | 24 +++ .../SendImmediateNotificationEmailMessage.php | 30 ++++ .../Notification/Email/NotificationMailer.php | 169 +++++++++++++++--- .../email_daily_digest.fr.md.twig | 18 ++ 9 files changed, 453 insertions(+), 26 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationEmailHandler.php create mode 100644 src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendDailyDigestHandler.php create mode 100644 src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php create mode 100644 src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationEmailMessage.php create mode 100644 src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendDailyDigestMessage.php create mode 100644 src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Notification/email_daily_digest.fr.md.twig diff --git a/config/packages/cache_chill.yaml b/config/packages/cache_chill.yaml index 71877b6dd..08f66bc91 100644 --- a/config/packages/cache_chill.yaml +++ b/config/packages/cache_chill.yaml @@ -12,3 +12,7 @@ framework: adapter: cache.adapter.redis public: false default_lifetime: 300 + cache.daily_notifications: + adapter: cache.adapter.redis + public: true + default_lifetime: 90000 # 25 hours diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationEmailHandler.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationEmailHandler.php new file mode 100644 index 000000000..cbc591564 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationEmailHandler.php @@ -0,0 +1,70 @@ +getAddresseeId(); + $notificationId = $message->getNotificationId(); + + // Store notification in cache grouped by user + $cacheKey = "daily_notifications_user_{$userId}"; + $existingNotifications = $this->dailyNotificationsCache->get($cacheKey, function () { + return []; + }); + + $existingNotifications[] = $notificationId; + + $this->dailyNotificationsCache->get($cacheKey, function () use ($existingNotifications) { + return $existingNotifications; + }); + + // Only send the daily digest message if this is the first notification for today otherwise it already exists + if (1 === count($existingNotifications)) { + $digestMessage = new SendDailyDigestMessage($userId); + + // Calculate delay until next 9 AM + $now = new \DateTimeImmutable(); + $nextNineAM = $now->modify('tomorrow 09:00'); + $delay = $nextNineAM->getTimestamp() - $now->getTimestamp(); + + $this->messageBus->dispatch($digestMessage, [ + new DelayStamp($delay * 1000), + ]); + } + + $this->logger->info('[ScheduleDailyNotificationEmailHandler] Added notification to daily cache', [ + 'notification_id' => $notificationId, + 'user_id' => $userId, + 'total_pending' => count($existingNotifications), + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendDailyDigestHandler.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendDailyDigestHandler.php new file mode 100644 index 000000000..a45b6381f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendDailyDigestHandler.php @@ -0,0 +1,66 @@ +getUserId(); + $cacheKey = "daily_notifications_user_{$userId}"; + + $notificationIds = $this->dailyNotificationsCache->get($cacheKey, []); + + if (empty($notificationIds)) { + $this->logger->info('[SendDailyDigestHandler] No notifications found for user', [ + 'user_id' => $userId, + ]); + + return; + } + + $user = $this->userRepository->find($userId); + $notifications = $this->notificationRepository->findBy(['id' => $notificationIds]); + + if ($user && !empty($notifications)) { + $this->notificationMailer->sendDailyDigest($user, $notifications); + + // Clear the cache after sending + $this->dailyNotificationsCache->delete($cacheKey); + + $this->logger->info('[SendDailyDigestHandler] Sent daily digest', [ + 'user_id' => $userId, + 'notification_count' => count($notifications), + ]); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php new file mode 100644 index 000000000..c81653639 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php @@ -0,0 +1,68 @@ +notificationRepository->find($message->getNotificationId()); + $addressee = $this->userRepository->find($message->getAddresseeId()); + + if (null === $notification) { + $this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [ + 'notification_id' => $message->getNotificationId(), + ]); + + return; + } + + if (null === $addressee) { + $this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [ + 'addressee_id' => $message->getAddresseeId(), + ]); + + return; + } + + try { + $this->notificationMailer->sendEmailToAddressee($notification, $addressee); + } catch (\Exception $e) { + $this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [ + 'notification_id' => $message->getNotificationId(), + 'addressee_id' => $message->getAddresseeId(), + 'error' => $e->getMessage(), + ]); + throw $e; + } + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationEmailMessage.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationEmailMessage.php new file mode 100644 index 000000000..8e96e8770 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationEmailMessage.php @@ -0,0 +1,30 @@ +notificationId; + } + + public function getAddresseeId(): int + { + return $this->addresseeId; + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendDailyDigestMessage.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendDailyDigestMessage.php new file mode 100644 index 000000000..770a2077b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendDailyDigestMessage.php @@ -0,0 +1,24 @@ +userId; + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php new file mode 100644 index 000000000..fb9908b21 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php @@ -0,0 +1,30 @@ +notificationId; + } + + public function getAddresseeId(): int + { + return $this->addresseeId; + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php index 7b535f1a7..34e6f031a 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php @@ -13,6 +13,8 @@ namespace Chill\MainBundle\Notification\Email; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\NotificationComment; +use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationEmailMessage; +use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage; use Doctrine\ORM\Event\PostPersistEventArgs; use Doctrine\ORM\Event\PostUpdateEventArgs; use Psr\Log\LoggerInterface; @@ -20,11 +22,12 @@ use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Email; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Contracts\Translation\TranslatorInterface; -class NotificationMailer +readonly class NotificationMailer { - public function __construct(private readonly MailerInterface $mailer, private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator) {} + public function __construct(private MailerInterface $mailer, private LoggerInterface $logger, private MessageBusInterface $messageBus, private readonly TranslatorInterface $translator) {} public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void { @@ -80,38 +83,152 @@ class NotificationMailer private function sendNotificationEmailsToAddresses(Notification $notification): void { + if (null === $notification->getType()) { + $this->logger->warning('[NotificationMailer] Notification has no type, skipping email processing', [ + 'notification_id' => $notification->getId(), + ]); + + return; + } + foreach ($notification->getAddressees() as $addressee) { if (null === $addressee->getEmail()) { continue; } - if ($notification->isSystem()) { - $email = new Email(); - $email - ->text($notification->getMessage()); - } else { - $email = new TemplatedEmail(); - $email - ->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig') - ->context([ - 'notification' => $notification, - 'dest' => $addressee, - ]); - } + $this->processNotificationForAddressee($notification, $addressee); + } + } + private function processNotificationForAddressee(Notification $notification, $addressee): void + { + $notificationFlags = $addressee->getNotificationFlags(); + $notificationType = $notification->getType(); + + $emailPreference = $notificationFlags[$notificationType->value] ?? null; + + match ($emailPreference) { + 'immediate-email' => $this->scheduleImmediateEmail($notification, $addressee), + 'daily-email' => $this->scheduleDailyEmail($notification, $addressee), + default => $this->logger->debug('[NotificationMailer] No email preference set for notification type', [ + 'notification_id' => $notification->getId(), + 'addressee_email' => $addressee->getEmail(), + 'notification_type' => $notificationType->value, + 'preference' => $emailPreference, + ]), + }; + } + + private function scheduleImmediateEmail(Notification $notification, $addressee): void + { + $message = new SendImmediateNotificationEmailMessage( + $notification->getId(), + $addressee->getId() + ); + + $this->messageBus->dispatch($message); + + $this->logger->info('[NotificationMailer] Scheduled immediate email', [ + 'notification_id' => $notification->getId(), + 'addressee_email' => $addressee->getEmail(), + ]); + } + + private function scheduleDailyEmail(Notification $notification, $addressee): void + { + $message = new ScheduleDailyNotificationEmailMessage( + $notification->getId(), + $addressee->getId() + ); + + $this->messageBus->dispatch($message); + + $this->logger->info('[NotificationMailer] Scheduled daily email', [ + 'notification_id' => $notification->getId(), + 'addressee_email' => $addressee->getEmail(), + ]); + } + + /** + * This method sends the email but is now called by the immediate notification email message handler. + * + * @throws TransportExceptionInterface + */ + public function sendEmailToAddressee(Notification $notification, $addressee): void + { + if (null === $addressee->getEmail()) { + return; + } + + if ($notification->isSystem()) { + $email = new Email(); + $email->text($notification->getMessage()); + } else { + $email = new TemplatedEmail(); $email - ->subject($notification->getTitle()) - ->to($addressee->getEmail()); - - try { - $this->mailer->send($email); - } catch (TransportExceptionInterface $e) { - $this->logger->warning('[NotificationMailer] could not send an email notification', [ - 'to' => $addressee->getEmail(), - 'error_message' => $e->getMessage(), - 'error_trace' => $e->getTraceAsString(), + ->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig') + ->context([ + 'notification' => $notification, + 'dest' => $addressee, ]); - } + } + + $email + ->subject($notification->getTitle()) + ->to($addressee->getEmail()); + + try { + $this->mailer->send($email); + $this->logger->info('[NotificationMailer] Email sent successfully', [ + 'notification_id' => $notification->getId(), + 'addressee_email' => $addressee->getEmail(), + ]); + } catch (TransportExceptionInterface $e) { + $this->logger->warning('[NotificationMailer] Could not send an email notification', [ + 'to' => $addressee->getEmail(), + 'notification_id' => $notification->getId(), + 'error_message' => $e->getMessage(), + 'error_trace' => $e->getTraceAsString(), + ]); + throw $e; + } + } + + /** + * Send daily digest email with multiple notifications to a user. + * @throws TransportExceptionInterface + */ + public function sendDailyDigest($user, array $notifications): void + { + if (null === $user->getEmail() || empty($notifications)) { + return; + } + + $email = new TemplatedEmail(); + $email + ->textTemplate('@ChillMain/Notification/email_daily_digest.fr.md.twig') + ->context([ + 'user' => $user, + 'notifications' => $notifications, + 'notification_count' => count($notifications), + ]) + ->subject($this->translator->trans('notification.Daily Notification Digest')) + ->to($user->getEmail()); + + try { + $this->mailer->send($email); + $this->logger->info('[NotificationMailer] Daily digest email sent successfully', [ + 'user_email' => $user->getEmail(), + 'notification_count' => count($notifications), + ]); + } catch (TransportExceptionInterface $e) { + $this->logger->warning('[NotificationMailer] Could not send daily digest email', [ + 'to' => $user->getEmail(), + 'notification_count' => count($notifications), + 'error_message' => $e->getMessage(), + 'error_trace' => $e->getTraceAsString(), + ]); + throw $e; // Re-throw so the message handler can handle the failure } } diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_daily_digest.fr.md.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_daily_digest.fr.md.twig new file mode 100644 index 000000000..20d916d73 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_daily_digest.fr.md.twig @@ -0,0 +1,18 @@ +# Résumé quotidien des notifications + +Bonjour {{ user.name ?? user.email }}, + +Voici vos {{ notification_count }} notification{% if notification_count > 1 %}s{% endif %} du jour : + +{% for notification in notifications %} +## {{ notification.title }} + +{{ notification.message }} + +Vous pouvez visualiser la notification et y répondre ici: + +{{ absolute_url(path('chill_main_notification_show', {'_locale': 'fr', 'id': notification.id }, false)) }} +{% endfor %} + +-- +Le logiciel Chill