mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-07-08 01:46:13 +00:00
Implement cronjob logic for sending daily digest emails for notifications
This commit is contained in:
parent
1a99906339
commit
48bc019932
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Notification\Email;
|
||||
|
||||
use Chill\MainBundle\Cron\CronJobInterface;
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
readonly class DailyNotificationDigestCronjob implements CronJobInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ClockInterface $clock,
|
||||
private Connection $connection,
|
||||
private MessageBusInterface $messageBus,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||
{
|
||||
$now = $this->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'),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Notification\Email\NotificationEmailHandlers;
|
||||
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
|
||||
use Chill\MainBundle\Notification\Email\NotificationMailer;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
readonly class ScheduleDailyNotificationDigestHandler
|
||||
{
|
||||
public function __construct(
|
||||
private NotificationRepository $notificationRepository,
|
||||
private UserRepository $userRepository,
|
||||
private NotificationMailer $notificationMailer,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function __invoke(ScheduleDailyNotificationDigestMessage $message): void
|
||||
{
|
||||
$userId = $message->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),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Notification\Email\NotificationEmailHandlers;
|
||||
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationEmailMessage;
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendDailyDigestMessage;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Messenger\Stamp\DelayStamp;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
readonly class ScheduleDailyNotificationEmailHandler
|
||||
{
|
||||
public function __construct(
|
||||
private CacheInterface $dailyNotificationsCache,
|
||||
private MessageBusInterface $messageBus,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __invoke(ScheduleDailyNotificationEmailMessage $message): void
|
||||
{
|
||||
$userId = $message->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),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Notification\Email\NotificationEmailHandlers;
|
||||
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendDailyDigestMessage;
|
||||
use Chill\MainBundle\Notification\Email\NotificationMailer;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
#[AsMessageHandler]
|
||||
class SendDailyDigestHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CacheInterface $dailyNotificationsCache,
|
||||
private readonly NotificationRepository $notificationRepository,
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly NotificationMailer $notificationMailer,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __invoke(SendDailyDigestMessage $message): void
|
||||
{
|
||||
$userId = $message->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Notification\Email\NotificationEmailMessages;
|
||||
|
||||
readonly class ScheduleDailyNotificationEmailMessage
|
||||
{
|
||||
public function __construct(
|
||||
private int $notificationId,
|
||||
private int $addresseeId,
|
||||
) {}
|
||||
|
||||
public function getNotificationId(): int
|
||||
{
|
||||
return $this->notificationId;
|
||||
}
|
||||
|
||||
public function getAddresseeId(): int
|
||||
{
|
||||
return $this->addresseeId;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user