From 28c952521f43f2cad7a0a647fb638bb30ba0f349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 14 Jun 2022 01:15:58 +0200 Subject: [PATCH] command for sending bulk sms with tests --- .../SendShortMessageOnEligibleCalendar.php | 41 ++++++ .../ChillCalendarBundle/Entity/Calendar.php | 4 +- .../Repository/CalendarRepository.php | 11 +- .../short_message_canceled.twig | 1 + .../BulkCalendarShortMessageSender.php | 64 +++++++++ .../CalendarForShortMessageProvider.php | 69 ++++++++++ .../DefaultShortMessageForCalendarBuilder.php | 25 ++-- .../ShortMessageNotification/Generator.php | 28 ---- .../BulkCalendarShortMessageSenderTest.php | 128 ++++++++++++++++++ .../CalendarForShortMessageProviderTest.php | 103 ++++++++++++++ .../DefaultRangeGeneratorTest.php | 9 +- ...aultShortMessageForCalendarBuilderTest.php | 108 +++++++++++++++ .../migrations/Version20220613202636.php | 33 +++++ .../Service/ShortMessage/ShortMessage.php | 4 + .../ShortMessage/ShortMessageHandler.php | 32 +++++ 15 files changed, 616 insertions(+), 44 deletions(-) create mode 100644 src/Bundle/ChillCalendarBundle/Command/SendShortMessageOnEligibleCalendar.php create mode 100644 src/Bundle/ChillCalendarBundle/Resources/views/CalendarShortMessage/short_message_canceled.twig create mode 100644 src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/BulkCalendarShortMessageSender.php create mode 100644 src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/CalendarForShortMessageProvider.php delete mode 100644 src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/Generator.php create mode 100644 src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/BulkCalendarShortMessageSenderTest.php create mode 100644 src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/CalendarForShortMessageProviderTest.php create mode 100644 src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilderTest.php create mode 100644 src/Bundle/ChillCalendarBundle/migrations/Version20220613202636.php create mode 100644 src/Bundle/ChillMainBundle/Service/ShortMessage/ShortMessageHandler.php diff --git a/src/Bundle/ChillCalendarBundle/Command/SendShortMessageOnEligibleCalendar.php b/src/Bundle/ChillCalendarBundle/Command/SendShortMessageOnEligibleCalendar.php new file mode 100644 index 000000000..091436561 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Command/SendShortMessageOnEligibleCalendar.php @@ -0,0 +1,41 @@ +messageSender = $messageSender; + } + + public function getName() + { + return 'chill:calendar:send-short-messages'; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->messageSender->sendBulkMessageToEligibleCalendars(); + + return 0; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php index ebfb48dc5..da024fa94 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php @@ -567,9 +567,11 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface return $this; } - public function setSmsStatus(string $smsStatus): void + public function setSmsStatus(string $smsStatus): self { $this->smsStatus = $smsStatus; + + return $this; } public function setStartDate(DateTimeImmutable $startDate): self diff --git a/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php b/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php index 937c18d05..ef803a1b3 100644 --- a/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php +++ b/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php @@ -100,17 +100,22 @@ class CalendarRepository implements ObjectRepository $qb->where( $qb->expr()->andX( - $qb->expr()->eq('c.sendSMS', "'TRUE'"), + $qb->expr()->eq('c.sendSMS', ':true'), $qb->expr()->gte('c.startDate', ':startDate'), $qb->expr()->lt('c.startDate', ':endDate'), - $qb->expr()->in('c.smsStatus', ':statuses') + $qb->expr()->orX( + $qb->expr()->eq('c.smsStatus', ':pending'), + $qb->expr()->eq('c.smsStatus', ':cancel_pending') + ) ) ); $qb->setParameters([ + 'true' => true, 'startDate' => $startDate, 'endDate' => $endDate, - 'statuses' => [Calendar::SMS_PENDING, Calendar::SMS_CANCEL_PENDING], + 'pending' => Calendar::SMS_PENDING, + 'cancel_pending' => Calendar::SMS_CANCEL_PENDING, ]); return $qb; diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/CalendarShortMessage/short_message_canceled.twig b/src/Bundle/ChillCalendarBundle/Resources/views/CalendarShortMessage/short_message_canceled.twig new file mode 100644 index 000000000..82453a7a9 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/CalendarShortMessage/short_message_canceled.twig @@ -0,0 +1 @@ +Votre RDV avec votre travailleur social {{ calendar.mainUser.label }} prévu le {{ calendar.startDate|format_date('short', locale='fr') }} à {{ calendar.startDate|format_time('short', locale='fr') }} est annulé. {% if calendar.mainUser.mainLocation is not null and calendar.mainUser.mainLocation.phonenumber1 is not null %} En cas de question, appelez-nous au {{ calendar.mainUser.mainLocation.phonenumber1|chill_format_phonenumber }}.{% endif %} diff --git a/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/BulkCalendarShortMessageSender.php b/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/BulkCalendarShortMessageSender.php new file mode 100644 index 000000000..06a67c1ce --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/BulkCalendarShortMessageSender.php @@ -0,0 +1,64 @@ +provider = $provider; + $this->em = $em; + $this->logger = $logger; + $this->messageBus = $messageBus; + $this->messageForCalendarBuilder = $messageForCalendarBuilder; + } + + public function sendBulkMessageToEligibleCalendars() + { + $countCalendars = 0; + $countSms = 0; + + foreach ($this->provider->getCalendars(new DateTimeImmutable('now')) as $calendar) { + $smses = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar); + + foreach ($smses as $sms) { + $this->messageBus->dispatch($sms); + ++$countSms; + } + + $this->em + ->createQuery('UPDATE ' . Calendar::class . ' c SET c.smsStatus = :smsStatus WHERE c.id = :id') + ->setParameters(['smsStatus' => Calendar::SMS_SENT, 'id' => $calendar->getId()]) + ->execute(); + ++$countCalendars; + $this->em->refresh($calendar); + } + + $this->logger->info(__CLASS__ . 'a bulk of messages was sent', ['count_calendars' => $countCalendars, 'count_sms' => $countSms]); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/CalendarForShortMessageProvider.php b/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/CalendarForShortMessageProvider.php new file mode 100644 index 000000000..feb3b698b --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/CalendarForShortMessageProvider.php @@ -0,0 +1,69 @@ +calendarRepository = $calendarRepository; + $this->em = $em; + $this->rangeGenerator = $rangeGenerator; + } + + /** + * Generate calendars instance. + * + * Warning: this method takes care of clearing the EntityManager at regular interval + * + * @return iterable|Calendar[] + */ + public function getCalendars(DateTimeImmutable $at): iterable + { + ['startDate' => $startDate, 'endDate' => $endDate] = $this->rangeGenerator + ->generateRange($at); + + $offset = 0; + $batchSize = 10; + + $calendars = $this->calendarRepository + ->findByNotificationAvailable($startDate, $endDate, $batchSize, $offset); + + do { + foreach ($calendars as $calendar) { + ++$offset; + + yield $calendar; + } + + $this->em->clear(); + + $calendars = $this->calendarRepository + ->findByNotificationAvailable($startDate, $endDate, $batchSize, $offset); + } while (count($calendars) === $batchSize); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilder.php b/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilder.php index 802605179..2abb3cb00 100644 --- a/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilder.php +++ b/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilder.php @@ -12,26 +12,22 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Service\ShortMessageNotification; use Chill\CalendarBundle\Entity\Calendar; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Chill\MainBundle\Service\ShortMessage\ShortMessage; use Symfony\Component\Templating\EngineInterface; class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBuilderInterface { - private ?array $config = null; - private EngineInterface $engine; public function __construct( - ParameterBagInterface $parameterBag, EngineInterface $engine ) { - $this->config = $parameterBag->get('chill_calendar.short_messages'); $this->engine = $engine; } public function buildMessageForCalendar(Calendar $calendar): array { - if (null === $this->config || true !== $calendar->getSendSMS()) { + if (true !== $calendar->getSendSMS()) { return []; } @@ -42,10 +38,19 @@ class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBu continue; } - $toUsers[] = new \Chill\MainBundle\Service\ShortMessage\ShortMessage( - $this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]), - $person->getMobilenumber() - ); + if (Calendar::SMS_PENDING === $calendar->getSmsStatus()) { + $toUsers[] = new ShortMessage( + $this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]), + $person->getMobilenumber(), + ShortMessage::PRIORITY_LOW + ); + } elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus()) { + $toUsers[] = new ShortMessage( + $this->engine->render('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig', ['calendar' => $calendar]), + $person->getMobilenumber(), + ShortMessage::PRIORITY_LOW + ); + } } return $toUsers; diff --git a/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/Generator.php b/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/Generator.php deleted file mode 100644 index d78faff4c..000000000 --- a/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/Generator.php +++ /dev/null @@ -1,28 +0,0 @@ -get(EntityManagerInterface::class); + + foreach ($this->toDelete as [$entity, $id]) { + $entity = $em->find($entity, $id); + $em->remove($entity); + } + + $em->flush(); + } + + public function testSendBulkMessageToEligibleCalendar() + { + $em = self::$container->get(EntityManagerInterface::class); + $calendar = new Calendar(); + $calendar + ->addPerson($this->getRandomPerson($em)) + ->setMainUser($user = $this->prepareUser([])) + ->setStartDate(new DateTimeImmutable('now')) + ->setEndDate($calendar->getStartDate()->add(new DateInterval('PT30M'))) + ->setSendSMS(true); + + $user->setUsername(uniqid()); + $user->setEmail(uniqid() . '@gmail.com'); + $calendar->getPersons()->first()->setAcceptSMS(true); + + // hack to prevent side effect with messages + $calendar->preventEnqueueChanges = true; + + $em->persist($user); + //$this->toDelete[] = [User::class, $user->getId()]; + $em->persist($calendar); + //$this->toDelete[] = [Calendar::class, $calendar->getId()]; + $em->flush(); + + $provider = $this->prophesize(CalendarForShortMessageProvider::class); + $provider->getCalendars(Argument::type(DateTimeImmutable::class)) + ->willReturn(new ArrayIterator([$calendar])); + + $messageBuilder = $this->prophesize(ShortMessageForCalendarBuilderInterface::class); + $messageBuilder->buildMessageForCalendar(Argument::type(Calendar::class)) + ->willReturn( + [ + new ShortMessage( + 'content', + PhoneNumberUtil::getInstance()->parse('+32470123456', 'BE'), + ShortMessage::PRIORITY_MEDIUM + ), + ] + ); + + $bus = $this->prophesize(MessageBusInterface::class); + $bus->dispatch(Argument::type(ShortMessage::class)) + ->willReturn(new Envelope(new stdClass())) + ->shouldBeCalledTimes(1); + + $bulk = new BulkCalendarShortMessageSender( + $provider->reveal(), + $em, + new NullLogger(), + $bus->reveal(), + $messageBuilder->reveal() + ); + + $bulk->sendBulkMessageToEligibleCalendars(); + + $em->clear(); + $calendar = $em->find(Calendar::class, $calendar->getId()); + + $this->assertEquals(Calendar::SMS_SENT, $calendar->getSmsStatus()); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/CalendarForShortMessageProviderTest.php b/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/CalendarForShortMessageProviderTest.php new file mode 100644 index 000000000..ffeefa213 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/CalendarForShortMessageProviderTest.php @@ -0,0 +1,103 @@ +prophesize(CalendarRepository::class); + $calendarRepository->findByNotificationAvailable( + Argument::type(DateTimeImmutable::class), + Argument::type(DateTimeImmutable::class), + Argument::type('int'), + Argument::exact(0) + )->will(static function ($args) { + return array_fill(0, $args[2], new Calendar()); + })->shouldBeCalledTimes(1); + $calendarRepository->findByNotificationAvailable( + Argument::type(DateTimeImmutable::class), + Argument::type(DateTimeImmutable::class), + Argument::type('int'), + Argument::not(0) + )->will(static function ($args) { + return array_fill(0, $args[2] - 1, new Calendar()); + })->shouldBeCalledTimes(1); + + $em = $this->prophesize(EntityManagerInterface::class); + $em->clear()->shouldBeCalled(); + + $provider = new CalendarForShortMessageProvider( + $calendarRepository->reveal(), + $em->reveal(), + new DefaultRangeGenerator() + ); + + $calendars = iterator_to_array($provider->getCalendars(new DateTimeImmutable('now'))); + + $this->assertGreaterThan(1, count($calendars)); + $this->assertLessThan(100, count($calendars)); + $this->assertContainsOnly(Calendar::class, $calendars); + } + + public function testGetCalendarsWithOnlyOneCalendar() + { + $calendarRepository = $this->prophesize(CalendarRepository::class); + $calendarRepository->findByNotificationAvailable( + Argument::type(DateTimeImmutable::class), + Argument::type(DateTimeImmutable::class), + Argument::type('int'), + Argument::exact(0) + )->will(static function ($args) { + return array_fill(0, 1, new Calendar()); + })->shouldBeCalledTimes(1); + $calendarRepository->findByNotificationAvailable( + Argument::type(DateTimeImmutable::class), + Argument::type(DateTimeImmutable::class), + Argument::type('int'), + Argument::not(0) + )->will(static function ($args) { + return []; + })->shouldBeCalledTimes(1); + + $em = $this->prophesize(EntityManagerInterface::class); + $em->clear()->shouldBeCalled(); + + $provider = new CalendarForShortMessageProvider( + $calendarRepository->reveal(), + $em->reveal(), + new DefaultRangeGenerator() + ); + + $calendars = iterator_to_array($provider->getCalendars(new DateTimeImmutable('now'))); + + $this->assertEquals(1, count($calendars)); + $this->assertContainsOnly(Calendar::class, $calendars); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/DefaultRangeGeneratorTest.php b/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/DefaultRangeGeneratorTest.php index c910d4bf0..7d873c0c1 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/DefaultRangeGeneratorTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/DefaultRangeGeneratorTest.php @@ -83,7 +83,12 @@ final class DefaultRangeGeneratorTest extends TestCase ['startDate' => $actualStartDate, 'endDate' => $actualEndDate] = $generator->generateRange($date); - $this->assertEquals($startDate->format(DateTimeImmutable::ATOM), $actualStartDate->format(DateTimeImmutable::ATOM)); - $this->assertEquals($endDate->format(DateTimeImmutable::ATOM), $actualEndDate->format(DateTimeImmutable::ATOM)); + if (null === $startDate) { + $this->assertNull($actualStartDate); + $this->assertNull($actualEndDate); + } else { + $this->assertEquals($startDate->format(DateTimeImmutable::ATOM), $actualStartDate->format(DateTimeImmutable::ATOM)); + $this->assertEquals($endDate->format(DateTimeImmutable::ATOM), $actualEndDate->format(DateTimeImmutable::ATOM)); + } } } diff --git a/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilderTest.php b/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilderTest.php new file mode 100644 index 000000000..736bea81d --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilderTest.php @@ -0,0 +1,108 @@ +phoneNumberUtil = PhoneNumberUtil::getInstance(); + } + + public function testBuildMessageForCalendar() + { + $calendar = new Calendar(); + $calendar + ->setStartDate(new DateTimeImmutable('now')) + ->setEndDate($calendar->getStartDate()->add(new DateInterval('PT30M'))) + ->setMainUser($user = new User()) + ->addPerson($person = new Person()) + ->setSendSMS(false); + $user + ->setLabel('Alex') + ->setMainLocation($location = new Location()); + $location->setName('LOCAMAT'); + $person + ->setMobilenumber($this->phoneNumberUtil->parse('+32470123456', 'BE')) + ->setAcceptSMS(false); + + $engine = $this->prophesize(EngineInterface::class); + $engine->render(Argument::exact('@ChillCalendar/CalendarShortMessage/short_message.txt.twig'), Argument::withKey('calendar')) + ->willReturn('message content') + ->shouldBeCalledTimes(1); + $engine->render(Argument::exact('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig'), Argument::withKey('calendar')) + ->willReturn('message canceled') + ->shouldBeCalledTimes(1); + + $builder = new DefaultShortMessageForCalendarBuilder( + $engine->reveal() + ); + + // if the calendar should not send sms + $sms = $builder->buildMessageForCalendar($calendar); + $this->assertCount(0, $sms); + + // if the person do not accept sms + $calendar->setSendSMS(true); + $sms = $builder->buildMessageForCalendar($calendar); + $this->assertCount(0, $sms); + + // person accepts sms + $person->setAcceptSMS(true); + $sms = $builder->buildMessageForCalendar($calendar); + + $this->assertCount(1, $sms); + $this->assertEquals( + '+32470123456', + $this->phoneNumberUtil->format($sms[0]->getPhoneNumber(), PhoneNumberFormat::E164) + ); + $this->assertEquals('message content', $sms[0]->getContent()); + $this->assertEquals('low', $sms[0]->getPriority()); + + // if the calendar is canceled + $calendar + ->setSmsStatus(Calendar::SMS_SENT) + ->setStatus(Calendar::STATUS_CANCELED); + + $sms = $builder->buildMessageForCalendar($calendar); + + $this->assertCount(1, $sms); + $this->assertEquals( + '+32470123456', + $this->phoneNumberUtil->format($sms[0]->getPhoneNumber(), PhoneNumberFormat::E164) + ); + $this->assertEquals('message canceled', $sms[0]->getContent()); + $this->assertEquals('low', $sms[0]->getPriority()); + } +} diff --git a/src/Bundle/ChillCalendarBundle/migrations/Version20220613202636.php b/src/Bundle/ChillCalendarBundle/migrations/Version20220613202636.php new file mode 100644 index 000000000..9e9099384 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/migrations/Version20220613202636.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE chill_calendar.calendar DROP smsStatus'); + } + + public function getDescription(): string + { + return 'Add sms status on calendars'; + } + + public function up(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_calendar.calendar ADD smsStatus TEXT DEFAULT \'sms_pending\' NOT NULL'); + } +} diff --git a/src/Bundle/ChillMainBundle/Service/ShortMessage/ShortMessage.php b/src/Bundle/ChillMainBundle/Service/ShortMessage/ShortMessage.php index 49332733c..360dce5be 100644 --- a/src/Bundle/ChillMainBundle/Service/ShortMessage/ShortMessage.php +++ b/src/Bundle/ChillMainBundle/Service/ShortMessage/ShortMessage.php @@ -15,6 +15,10 @@ use libphonenumber\PhoneNumber; class ShortMessage { + public const PRIORITY_LOW = 'low'; + + public const PRIORITY_MEDIUM = 'medium'; + private string $content; private PhoneNumber $phoneNumber; diff --git a/src/Bundle/ChillMainBundle/Service/ShortMessage/ShortMessageHandler.php b/src/Bundle/ChillMainBundle/Service/ShortMessage/ShortMessageHandler.php new file mode 100644 index 000000000..805f809d9 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/ShortMessage/ShortMessageHandler.php @@ -0,0 +1,32 @@ +messageTransporter = $messageTransporter; + } + + public function __invoke(ShortMessage $message): void + { + $this->messageTransporter->send($message); + } +}