diff --git a/src/BusProNet/DataProvider/HotelDataProvider.php b/src/BusProNet/DataProvider/HotelDataProvider.php index 8807605..39e6ee6 100644 --- a/src/BusProNet/DataProvider/HotelDataProvider.php +++ b/src/BusProNet/DataProvider/HotelDataProvider.php @@ -5,6 +5,7 @@ namespace App\BusProNet\DataProvider; use App\BusProNet\ApiClient; use App\BusProNet\ApiClientException; use App\BusProNet\Model\Hotel; +use Psr\Cache\InvalidArgumentException; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; @@ -25,7 +26,7 @@ class HotelDataProvider ->getItems() ; }); - } catch (ApiClientException $e) { + } catch (ApiClientException|InvalidArgumentException $e) { $hotels = []; } diff --git a/src/Command/CronCommand.php b/src/Command/CronCommand.php index be61a34..fac1525 100644 --- a/src/Command/CronCommand.php +++ b/src/Command/CronCommand.php @@ -2,13 +2,9 @@ namespace App\Command; -use App\Email\Mailer; -use App\Entity\Disposition; -use App\Entity\Upload; -use Doctrine\DBAL\Exception; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Query\Expr\Join; -use Psr\Log\LoggerInterface; +use App\Service\Cron\FeedbackReminderService; +use App\Service\Cron\FlushLogsService; +use App\Service\Cron\UploadReminderService; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -22,9 +18,9 @@ use Symfony\Component\Console\Style\SymfonyStyle; class CronCommand extends Command { public function __construct( - private readonly EntityManagerInterface $entityManager, - private readonly Mailer $mailer, - private readonly LoggerInterface $logger + private readonly FlushLogsService $flushLogsService, + private readonly UploadReminderService $uploadReminderService, + private readonly FeedbackReminderService $feedbackReminderService ) { parent::__construct(); } @@ -33,147 +29,22 @@ class CronCommand extends Command { $io = new SymfonyStyle($input, $output); - $this->flushLogs($io); - $this->sendContractUploadReminders($io); - $this->sendInvoiceUploadReminders($io); + [$success, $message] = $this->flushLogsService->flush(); + if (true === $success) { + $io->success($message); + } else { + $io->error($message); + } + + $message = $this->uploadReminderService->sendContractUploadReminders(); + $io->info($message); + + $message = $this->uploadReminderService->sendInvoiceUploadReminders(); + $io->info($message); + + $message = $this->feedbackReminderService->sendFeedbackReminders(); + $io->info($message); return Command::SUCCESS; } - - private function flushLogs(SymfonyStyle $io): void - { - // Deletes all log entries older than 30 days - $dueDate = (new \DateTimeImmutable())->modify('-30 days'); - try { - $result = $this - ->entityManager - ->getConnection() - ->executeQuery('DELETE FROM log WHERE log.timestamp < ?', [$dueDate->format('Y-m-d H:i:s')]); - $message = 'Deleted '.$result->rowCount().' log entries'; - $io->info($message); - $this->logger->info($message); - } catch (Exception $e) { - $message = 'Unable to delete log entries: '.$e->getMessage(); - $io->error($message); - $this->logger->error($message); - } - } - - private function sendContractUploadReminders(SymfonyStyle $io): void - { - // Find dispositions of assignments with due contract upload - $repository = $this->entityManager->getRepository(Disposition::class); - - // Contracts have to be uploaded before assignment's begin date. Reminder is - // sent three days before end of this period thus due date is today in three days. - $contractDueDate = (new \DateTimeImmutable())->modify('+3 days'); - - $qb = $repository->createQueryBuilder('disposition'); - - $dispositions = $qb - ->select('disposition', 'assignment', 'destination') - ->innerJoin('disposition.assignment', 'assignment') - ->innerJoin('assignment.destination', 'destination') - ->leftJoin('disposition.documents', 'document', Join::WITH, 'document.type = :documentType') - ->where($qb->expr()->andX( - $qb->expr()->isNull('document'), - $qb->expr()->orX( - // Due date can be determined by assignment's date which potentially overrides destination date - $qb->expr()->andX( - $qb->expr()->isNotNull('assignment.dateFrom'), - $qb->expr()->eq('assignment.dateFrom', ':dateFrom') - ), - $qb->expr()->eq('destination.dateFrom', ':dateFrom'), - ) - )) - ->setParameters([ - 'documentType' => Upload::TYPE_CONTRACT, - 'dateFrom' => $contractDueDate, - ]) - ->getQuery() - ->getResult() - ; - - if (0 === $count = count($dispositions)) { - $message = 'No reminders regarding due contract uploads to be sent'; - $io->info($message); - $this->logger->info($message); - return; - } - - foreach ($dispositions as $disposition) { - $teamer = $disposition->getTeamer(); - - $this->mailer->createAndSendEmail([ - 'disposition' => $disposition, - ], [ - 'to' => $teamer->getCommunication()->getEmail(), - 'subject' => 'Reminder: Fehlende Dokumente', - 'template' => 'email/reminder_contract_upload.html.twig', - ]); - } - - $message = 'Sent '.$count.' reminders about due contract uploads'; - $io->info($message); - $this->logger->info($message); - } - - private function sendInvoiceUploadReminders(SymfonyStyle $io): void - { - // Find dispositions of assignments with due invoice upload - $repository = $this->entityManager->getRepository(Disposition::class); - - // Invoices have to be uploaded until 14 days after end of assignment. Reminder - // is sent three days before end of this period thus on the 11th day after end of assignment. - $invoiceDueDate = (new \DateTimeImmutable())->modify('-11 days'); - - $qb = $repository->createQueryBuilder('disposition'); - - $dispositions = $qb - ->select('disposition', 'assignment', 'destination') - ->innerJoin('disposition.assignment', 'assignment') - ->innerJoin('assignment.destination', 'destination') - ->leftJoin('disposition.documents', 'document', Join::WITH, 'document.type = :documentType') - ->where($qb->expr()->andX( - $qb->expr()->isNull('document'), - $qb->expr()->orX( - // Due date can be determined by assignment's date which potentially overrides destination date - $qb->expr()->andX( - $qb->expr()->isNotNull('assignment.dateTo'), - $qb->expr()->eq('assignment.dateTo', ':dateTo') - ), - $qb->expr()->eq('destination.dateTo', ':dateTo'), - ) - )) - ->setParameters([ - 'documentType' => Upload::TYPE_INVOICE, - 'dateTo' => $invoiceDueDate, - ]) - ->getQuery() - ->getResult() - ; - - if (0 === $count = count($dispositions)) { - $message = 'No reminders regarding due invoice uploads to be sent'; - $io->info($message); - $this->logger->info($message); - return; - } - - foreach ($dispositions as $disposition) { - $teamer = $disposition->getTeamer(); - - $this->mailer->createAndSendEmail([ - 'disposition' => $disposition, - ], [ - 'to' => $teamer->getCommunication()->getEmail(), - 'subject' => 'Reminder: Fehlende Dokumente', - 'template' => 'email/reminder_invoice_upload.html.twig', - ]); - } - - $message = 'Sent '.$count.' reminders about due invoice uploads'; - $io->info($message); - $this->logger->info($message); - } } diff --git a/src/Repository/DispositionRepository.php b/src/Repository/DispositionRepository.php index e582176..62538ae 100644 --- a/src/Repository/DispositionRepository.php +++ b/src/Repository/DispositionRepository.php @@ -157,6 +157,14 @@ class DispositionRepository extends ServiceEntityRepository return $qb->getQuery(); } + public function getPendingFeedbacks(): array + { + return $this + ->getPendingFeedbackQuery() + ->getResult() + ; + } + public function getPendingFeedbackByHotelBusProIds(array $hotelBusProIds): array { return $this diff --git a/src/Service/Cron/FeedbackReminderService.php b/src/Service/Cron/FeedbackReminderService.php new file mode 100644 index 0000000..13d0c0f --- /dev/null +++ b/src/Service/Cron/FeedbackReminderService.php @@ -0,0 +1,72 @@ +dispositionRepository->getPendingFeedbacks(); + + $sortedFeedbacks = []; + + foreach ($pendingFeedbacks as $feedback) { + // Get hotel by bus pro id + $hotelBusProId = $feedback->getDestination()->getHotelBusProId(); + $hotel = $this->hotelDataProvider->get($hotelBusProId); + + if (null === $hotel) { + $this->logger->error('Could not find hotel by bus pro id '.$hotelBusProId); + continue; + } + + // Get hotel base code to find hotel manager later + $hotelBaseCode = substr($hotel->getCode(), 0, 3); + + if (false === array_key_exists($hotelBaseCode, $sortedFeedbacks)) { + $sortedFeedbacks[$hotelBaseCode] = []; + } + + // Sort feedback by hotel base code + $sortedFeedbacks[$hotelBaseCode][] = $feedback; + } + + // Determine hotel managers to notify + $hotelBaseCodes = array_keys($sortedFeedbacks); + + /** @var User[] $hotelManagers */ + $hotelManagers = $this->userRepository->findBy([ + 'hotelCode' => $hotelBaseCodes, + ]); + + foreach ($hotelManagers as $manager) { + $this->mailer->createAndSendEmail([ + 'feedbacks' => $sortedFeedbacks[$manager->getHotelCode()], + ], [ + 'to' => $manager->getEmail(), + 'subject' => 'Reminder: Offene Feedbacks', + 'template' => 'email/reminder_pending_feedback.html.twig', + ]); + } + + return 'Sent feedback reminders to '.count($hotelManagers).' hotel managers'; + } +} \ No newline at end of file diff --git a/src/Service/Cron/FlushLogsService.php b/src/Service/Cron/FlushLogsService.php new file mode 100644 index 0000000..499134f --- /dev/null +++ b/src/Service/Cron/FlushLogsService.php @@ -0,0 +1,39 @@ +modify('-30 days'); + + try { + $result = $this + ->entityManager + ->getConnection() + ->executeQuery('DELETE FROM log WHERE log.timestamp < ?', [$dueDate->format('Y-m-d H:i:s')]); + $message = 'Deleted '.$result->rowCount().' log entries'; + $success = true; + $this->logger->info($message); + } catch (Exception $e) { + $message = 'Unable to delete log entries: '.$e->getMessage(); + $success = false; + $this->logger->error($message); + } + + return [$success, $message]; + } + +} \ No newline at end of file diff --git a/src/Service/Cron/UploadReminderService.php b/src/Service/Cron/UploadReminderService.php new file mode 100644 index 0000000..43d297d --- /dev/null +++ b/src/Service/Cron/UploadReminderService.php @@ -0,0 +1,137 @@ +modify('+3 days'); + + // Find dispositions of assignments with due contract upload + $qb = $this->dispositionRepository->createQueryBuilder('disposition'); + + $dispositions = $qb + ->select('disposition', 'assignment', 'destination') + ->innerJoin('disposition.assignment', 'assignment') + ->innerJoin('assignment.destination', 'destination') + ->leftJoin('disposition.documents', 'document', Join::WITH, 'document.type = :documentType') + ->where($qb->expr()->andX( + $qb->expr()->isNull('document'), + $qb->expr()->orX( + // Due date can be determined by assignment's date which potentially overrides destination date + $qb->expr()->andX( + $qb->expr()->isNotNull('assignment.dateFrom'), + $qb->expr()->eq('assignment.dateFrom', ':dateFrom') + ), + $qb->expr()->eq('destination.dateFrom', ':dateFrom'), + ) + )) + ->setParameters([ + 'documentType' => Upload::TYPE_CONTRACT, + 'dateFrom' => $contractDueDate, + ]) + ->getQuery() + ->getResult() + ; + + if (0 === $count = count($dispositions)) { + $message = 'No reminders regarding due contract uploads to be sent'; + $this->logger->info($message); + + return $message; + } + + foreach ($dispositions as $disposition) { + $teamer = $disposition->getTeamer(); + + $this->mailer->createAndSendEmail([ + 'disposition' => $disposition, + ], [ + 'to' => $teamer->getCommunication()->getEmail(), + 'subject' => 'Reminder: Fehlende Dokumente', + 'template' => 'email/reminder_contract_upload.html.twig', + ]); + } + + $message = 'Sent '.$count.' reminders about due contract uploads'; + $this->logger->info($message); + + return $message; + } + + public function sendInvoiceUploadReminders(): string + { + // Invoices have to be uploaded until 14 days after end of assignment. Reminder + // is sent three days before end of this period thus on the 11th day after end of assignment. + $invoiceDueDate = (new \DateTimeImmutable())->modify('-11 days'); + + // Find dispositions of assignments with due invoice upload + $qb = $this->dispositionRepository->createQueryBuilder('disposition'); + + $dispositions = $qb + ->select('disposition', 'assignment', 'destination') + ->innerJoin('disposition.assignment', 'assignment') + ->innerJoin('assignment.destination', 'destination') + ->leftJoin('disposition.documents', 'document', Join::WITH, 'document.type = :documentType') + ->where($qb->expr()->andX( + $qb->expr()->isNull('document'), + $qb->expr()->orX( + // Due date can be determined by assignment's date which potentially overrides destination date + $qb->expr()->andX( + $qb->expr()->isNotNull('assignment.dateTo'), + $qb->expr()->eq('assignment.dateTo', ':dateTo') + ), + $qb->expr()->eq('destination.dateTo', ':dateTo'), + ) + )) + ->setParameters([ + 'documentType' => Upload::TYPE_INVOICE, + 'dateTo' => $invoiceDueDate, + ]) + ->getQuery() + ->getResult() + ; + + if (0 === $count = count($dispositions)) { + $message = 'No reminders regarding due invoice uploads to be sent'; + $this->logger->info($message); + + return $message; + } + + foreach ($dispositions as $disposition) { + $teamer = $disposition->getTeamer(); + + $this->mailer->createAndSendEmail([ + 'disposition' => $disposition, + ], [ + 'to' => $teamer->getCommunication()->getEmail(), + 'subject' => 'Reminder: Fehlende Dokumente', + 'template' => 'email/reminder_invoice_upload.html.twig', + ]); + } + + $message = 'Sent '.$count.' reminders about due invoice uploads'; + $this->logger->info($message); + + return $message; + } +} \ No newline at end of file diff --git a/templates/email/reminder_pending_feedback.html.twig b/templates/email/reminder_pending_feedback.html.twig new file mode 100644 index 0000000..e4888bb --- /dev/null +++ b/templates/email/reminder_pending_feedback.html.twig @@ -0,0 +1,12 @@ +{% extends 'email/layout.html.twig' %} + +{% block body %} +

+ Reminder zu ausstehenden Feedbacks +

+

+ + Zum Portal + +

+{% endblock %} \ No newline at end of file diff --git a/templates/house_manager/index.html.twig b/templates/house_manager/index.html.twig index 2fe2336..f9e3b4b 100644 --- a/templates/house_manager/index.html.twig +++ b/templates/house_manager/index.html.twig @@ -35,7 +35,7 @@

- Offene Feedbacks + Ausstehende Feedbacks