diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index 87c5c95ed..e64a7434b 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -466,4 +466,15 @@ class Notification implements TrackUpdateInterface { return $this->type; } + + public function isSendImmediately(User $user): bool + { + $notificationFlags = $user->getNotificationFlags(); + $notificationType = $this->getType(); + + // Check if the user has a preference for this notification type + $emailPreference = $notificationFlags[$notificationType] ?? null; + + return 'immediate-email' === $emailPreference; + } } diff --git a/src/Bundle/ChillMainBundle/Notification/Email/DailyNotificationDigestCronjob.php b/src/Bundle/ChillMainBundle/Notification/Email/DailyNotificationDigestCronjob.php new file mode 100644 index 000000000..f7a9ec280 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/DailyNotificationDigestCronjob.php @@ -0,0 +1,97 @@ +clock->now(); + + if (null !== $cronJobExecution && $now->sub(new \DateInterval('PT23H45M')) < $cronJobExecution->getLastStart()) { + return false; + } + + // Run between 6 and 9 AM + return in_array((int) $now->format('H'), [6, 7, 8], true); + } + + public function getKey(): string + { + return 'daily-notification-digest'; + } + + /** + * @throws \DateInvalidOperationException + * @throws Exception + */ + public function run(array $lastExecutionData): ?array + { + $now = $this->clock->now(); + $lastExecution = isset($lastExecutionData['last_execution']) + ? new \DateTimeImmutable($lastExecutionData['last_execution']) + : $now->sub(new \DateInterval('P1D')); + + // Get distinct users who received notifications since the last execution + $sql = <<<'SQL' + SELECT DISTINCT cmnau.user_id + FROM chill_main_notification cmn + JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id + WHERE cmn.date >= :lastExecution AND cmn.date <= :now + SQL; + + $sqlStatement = $this->connection->prepare($sql); + $sqlStatement->bindValue('lastExecution', $lastExecution->format('Y-m-d H:i:s')); + $sqlStatement->bindValue('now', $now->format('Y-m-d H:i:s')); + $result = $sqlStatement->executeQuery(); + + $count = 0; + foreach ($result->fetchAllAssociative() as $row) { + $userId = (int) $row['user_id']; + + $message = new ScheduleDailyNotificationDigestMessage( + $userId, + $lastExecution, + $now + ); + + $this->messageBus->dispatch($message); + ++$count; + } + + $this->logger->info('[DailyNotificationDigestCronjob] Dispatched daily digest messages', [ + 'user_count' => $count, + 'last_execution' => $lastExecution->format('Y-m-d H:i:s'), + 'current_time' => $now->format('Y-m-d H:i:s'), + ]); + + return [ + 'last_execution' => $now->format('Y-m-d H:i:s'), + ]; + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationDigestHandler.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationDigestHandler.php new file mode 100644 index 000000000..468c5e2b0 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationDigestHandler.php @@ -0,0 +1,75 @@ +getUserId(); + $lastExecutionDate = $message->getLastExecutionDate(); + $currentDate = $message->getCurrentDate(); + + $user = $this->userRepository->find($userId); + if (null === $user) { + $this->logger->warning('[ScheduleDailyNotificationDigestHandler] User not found', [ + 'user_id' => $userId, + ]); + return; + } + + // Get all notifications for this user between last execution and current date + $notifications = $this->notificationRepository->findNotificationsForUserBetweenDates( + $userId, + $lastExecutionDate, + $currentDate + ); + + // Filter out notifications that were sent immediately + $dailyNotifications = array_filter($notifications, function ($notification) use ($user) { + return !$notification->isSendImmediately($user); + }); + + if (empty($dailyNotifications)) { + $this->logger->info('[ScheduleDailyNotificationDigestHandler] No daily notifications found for user', [ + 'user_id' => $userId, + ]); + return; + } + + $this->notificationMailer->sendDailyDigest($user, $dailyNotifications); + + $this->logger->info('[ScheduleDailyNotificationDigestHandler] Sent daily digest', [ + 'user_id' => $userId, + 'notification_count' => count($dailyNotifications), + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationEmailHandler.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationEmailHandler.php deleted file mode 100644 index cbc591564..000000000 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationEmailHandler.php +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index a45b6381f..000000000 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendDailyDigestHandler.php +++ /dev/null @@ -1,66 +0,0 @@ -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/NotificationEmailMessages/SendDailyDigestMessage.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationDigestMessage.php similarity index 50% rename from src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendDailyDigestMessage.php rename to src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationDigestMessage.php index 770a2077b..5b1aa9232 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendDailyDigestMessage.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationDigestMessage.php @@ -11,14 +11,26 @@ declare(strict_types=1); namespace Chill\MainBundle\Notification\Email\NotificationEmailMessages; -class SendDailyDigestMessage +readonly class ScheduleDailyNotificationDigestMessage { public function __construct( - private readonly int $userId, + private int $userId, + private \DateTimeInterface $lastExecutionDate, + private \DateTimeInterface $currentDate, ) {} public function getUserId(): int { return $this->userId; } + + public function getLastExecutionDate(): \DateTimeInterface + { + return $this->lastExecutionDate; + } + + public function getCurrentDate(): \DateTimeInterface + { + return $this->currentDate; + } } diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationEmailMessage.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationEmailMessage.php deleted file mode 100644 index 8e96e8770..000000000 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationEmailMessage.php +++ /dev/null @@ -1,30 +0,0 @@ -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 6df12f4fd..e15dca129 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php @@ -13,7 +13,6 @@ 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; @@ -104,15 +103,14 @@ readonly class NotificationMailer $notificationFlags = $addressee->getNotificationFlags(); $notificationType = $notification->getType(); - $emailPreference = $notificationFlags[$notificationType->value] ?? null; + $emailPreference = $notificationFlags[$notificationType] ?? null; match ($emailPreference) { 'immediate-email' => $this->scheduleImmediateEmail($notification, $addressee), - 'daily-email' => $this->scheduleDailyEmail($notification, $addressee), // don't do this (the cronjob will handle them later) default => $this->logger->debug('[NotificationMailer] No email preference set for notification type', [ 'notification_id' => $notification->getId(), 'addressee_email' => $addressee->getEmail(), - 'notification_type' => $notificationType->value, + 'notification_type' => $notificationType, 'preference' => $emailPreference, ]), }; @@ -133,21 +131,6 @@ readonly class NotificationMailer ]); } - 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. * @@ -195,6 +178,7 @@ readonly class NotificationMailer /** * Send daily digest email with multiple notifications to a user. + * * @throws TransportExceptionInterface */ public function sendDailyDigest($user, array $notifications): void diff --git a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php index fb79a7397..90d6b8e2a 100644 --- a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php @@ -393,4 +393,30 @@ final class NotificationRepository implements ObjectRepository return $nq->getResult(); } + + /** + * Find all notifications for a user that were created between two dates. + * + * @return array|Notification[] + */ + public function findNotificationsForUserBetweenDates(int $userId, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array + { + $rsm = new Query\ResultSetMappingBuilder($this->em); + $rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn'); + + $sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '. + 'FROM chill_main_notification cmn '. + 'JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id '. + 'WHERE cmnau.user_id = :userId '. + 'AND cmn.date >= :startDate '. + 'AND cmn.date <= :endDate '. + 'ORDER BY cmn.date DESC'; + + $nq = $this->em->createNativeQuery($sql, $rsm) + ->setParameter('userId', $userId) + ->setParameter('startDate', $startDate, Types::DATETIME_MUTABLE) + ->setParameter('endDate', $endDate, Types::DATETIME_MUTABLE); + + return $nq->getResult(); + } } diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php index 26a9b5980..6ca46029a 100644 --- a/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php @@ -105,6 +105,29 @@ final class NotificationTest extends KernelTestCase $this->assertNotContains('other', $notification->getAddressesEmailsAdded()); } + public function testIsSendImmediately(): void + { + $notification = new Notification(); + $notification->setType('test_notification_type'); + + $user = new User(); + + // no notification flags + $this->assertFalse($notification->isSendImmediately($user), 'Should return false when no notification flags are set'); + + // immediate-email preference + $user->setNotificationFlags(['test_notification_type' => 'immediate-email']); + $this->assertTrue($notification->isSendImmediately($user), 'Should return true when preference is immediate-email'); + + // daily-email preference + $user->setNotificationFlags(['test_notification_type' => 'daily-email']); + $this->assertFalse($notification->isSendImmediately($user), 'Should return false when preference is daily-email'); + + // a different notification type + $notification->setType('other_notification_type'); + $this->assertFalse($notification->isSendImmediately($user), 'Should return false when notification type does not match any preference'); + } + /** * @dataProvider generateNotificationData */