mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-30 14:06:13 +00:00
WIP implement immediate and daily email functionality
This commit is contained in:
parent
c2782be56a
commit
e6cbba8c63
@ -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
|
||||
|
@ -0,0 +1,70 @@
|
||||
<?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),
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
<?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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
<?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\SendImmediateNotificationEmailMessage;
|
||||
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]
|
||||
class SendImmediateNotificationEmailHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NotificationRepository $notificationRepository,
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly NotificationMailer $notificationMailer,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __invoke(SendImmediateNotificationEmailMessage $message): void
|
||||
{
|
||||
$notification = $this->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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?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;
|
||||
|
||||
class SendDailyDigestMessage
|
||||
{
|
||||
public function __construct(
|
||||
private readonly int $userId,
|
||||
) {}
|
||||
|
||||
public function getUserId(): int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?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 SendImmediateNotificationEmailMessage
|
||||
{
|
||||
public function __construct(
|
||||
private int $notificationId,
|
||||
private int $addresseeId,
|
||||
) {}
|
||||
|
||||
public function getNotificationId(): int
|
||||
{
|
||||
return $this->notificationId;
|
||||
}
|
||||
|
||||
public function getAddresseeId(): int
|
||||
{
|
||||
return $this->addresseeId;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user