From 40aa7b2f35ba891fa0fbe58ef05a53722ebd586c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 15 Oct 2025 16:41:44 +0200 Subject: [PATCH] Add `MotiveUpdateEvent` and integrate dispatch logic in `ReplaceMotiveCommandHandler` - Introduced `MotiveUpdateEvent` to encapsulate motive updates for tickets. - Modified `ReplaceMotiveCommandHandler` to dispatch the new event when motives are updated. - Updated tests to validate `MotiveUpdateEvent` dispatching. --- .../Handler/ReplaceMotiveCommandHandler.php | 13 +++++ .../src/Event/MotiveUpdateEvent.php | 31 ++++++++++ .../ReplaceMotiveCommandHandlerTest.php | 57 ++++++++++++++++--- 3 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 src/Bundle/ChillTicketBundle/src/Event/MotiveUpdateEvent.php diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ReplaceMotiveCommandHandler.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ReplaceMotiveCommandHandler.php index a39186efe..985031156 100644 --- a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ReplaceMotiveCommandHandler.php +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ReplaceMotiveCommandHandler.php @@ -15,8 +15,11 @@ use Chill\TicketBundle\Action\Ticket\ChangeEmergencyStateCommand; use Chill\TicketBundle\Action\Ticket\ReplaceMotiveCommand; use Chill\TicketBundle\Entity\MotiveHistory; use Chill\TicketBundle\Entity\Ticket; +use Chill\TicketBundle\Event\MotiveUpdateEvent; +use Chill\TicketBundle\Event\TicketUpdateEvent; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Clock\ClockInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class ReplaceMotiveCommandHandler { @@ -24,6 +27,7 @@ class ReplaceMotiveCommandHandler private readonly ClockInterface $clock, private readonly EntityManagerInterface $entityManager, private readonly ChangeEmergencyStateCommandHandler $changeEmergencyStateCommandHandler, + private readonly EventDispatcherInterface $eventDispatcher, ) {} public function handle(Ticket $ticket, ReplaceMotiveCommand $command): void @@ -32,6 +36,8 @@ class ReplaceMotiveCommandHandler throw new \InvalidArgumentException('The new motive cannot be null'); } + $event = new MotiveUpdateEvent($ticket); + // will add if there are no existing motive $readyToAdd = 0 === count($ticket->getMotiveHistories()); @@ -45,6 +51,8 @@ class ReplaceMotiveCommandHandler continue; } + // collect previous active motives before closing + $event->previousMotive = $history->getMotive(); $history->setEndDate($this->clock->now()); $readyToAdd = true; } @@ -52,6 +60,7 @@ class ReplaceMotiveCommandHandler if ($readyToAdd) { $history = new MotiveHistory($command->motive, $ticket, $this->clock->now()); $this->entityManager->persist($history); + $event->newMotive = $command->motive; // Check if the motive has makeTicketEmergency set and update the ticket's emergency status if needed if ($command->motive->isMakeTicketEmergency()) { @@ -59,5 +68,9 @@ class ReplaceMotiveCommandHandler ($this->changeEmergencyStateCommandHandler)($ticket, $changeEmergencyCommand); } } + + if ($event->hasChanges()) { + $this->eventDispatcher->dispatch($event, TicketUpdateEvent::class); + } } } diff --git a/src/Bundle/ChillTicketBundle/src/Event/MotiveUpdateEvent.php b/src/Bundle/ChillTicketBundle/src/Event/MotiveUpdateEvent.php new file mode 100644 index 000000000..59aadf054 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Event/MotiveUpdateEvent.php @@ -0,0 +1,31 @@ +newMotive || null !== $this->previousMotive; + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ReplaceMotiveCommandHandlerTest.php b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ReplaceMotiveCommandHandlerTest.php index 17b1e88d9..27f98f35f 100644 --- a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ReplaceMotiveCommandHandlerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ReplaceMotiveCommandHandlerTest.php @@ -19,16 +19,19 @@ use Chill\TicketBundle\Entity\EmergencyStatusEnum; use Chill\TicketBundle\Entity\Motive; use Chill\TicketBundle\Entity\MotiveHistory; use Chill\TicketBundle\Entity\Ticket; +use Chill\TicketBundle\Event\MotiveUpdateEvent; +use Chill\TicketBundle\Event\TicketUpdateEvent; use Doctrine\ORM\EntityManagerInterface; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Clock\MockClock; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @internal * - * @coversNothing + * @covers \Chill\TicketBundle\Action\Ticket\Handler\ReplaceMotiveCommandHandler */ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase { @@ -37,14 +40,18 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase private function buildHandler( EntityManagerInterface $entityManager, ?ChangeEmergencyStateCommandHandler $changeEmergencyStateCommandHandler = null, + ?EventDispatcherInterface $eventDispatcher = null, ): ReplaceMotiveCommandHandler { $clock = new MockClock(); if (null === $changeEmergencyStateCommandHandler) { $changeEmergencyStateCommandHandler = $this->prophesize(ChangeEmergencyStateCommandHandler::class)->reveal(); } + if (null === $eventDispatcher) { + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class)->reveal(); + } - return new ReplaceMotiveCommandHandler($clock, $entityManager, $changeEmergencyStateCommandHandler); + return new ReplaceMotiveCommandHandler($clock, $entityManager, $changeEmergencyStateCommandHandler, $eventDispatcher); } public function testHandleOnTicketWithoutMotive(): void @@ -61,7 +68,18 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase return $arg->getMotive() === $motive; }))->shouldBeCalled(); - $handler = $this->buildHandler($entityManager->reveal()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch( + Argument::that(function ($event) use ($motive) { + return $event instanceof MotiveUpdateEvent + && $event->newMotive === $motive + && null === $event->previousMotive + && $event->hasChanges(); + }), + TicketUpdateEvent::class + )->shouldBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal(), null, $eventDispatcher->reveal()); $handler->handle($ticket, new ReplaceMotiveCommand($motive)); @@ -83,7 +101,19 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase return $arg->getMotive() === $motive; }))->shouldBeCalled(); - $handler = $this->buildHandler($entityManager->reveal()); + $previous = $history->getMotive(); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch( + Argument::that(function ($event) use ($motive, $previous) { + return $event instanceof MotiveUpdateEvent + && $event->newMotive === $motive + && $previous === $event->previousMotive + && $event->hasChanges(); + }), + TicketUpdateEvent::class + )->shouldBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal(), null, $eventDispatcher->reveal()); $handler->handle($ticket, new ReplaceMotiveCommand($motive)); @@ -106,7 +136,10 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase return $arg->getMotive() === $motive; }))->shouldNotBeCalled(); - $handler = $this->buildHandler($entityManager->reveal()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Argument::any(), TicketUpdateEvent::class)->shouldNotBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal(), null, $eventDispatcher->reveal()); $handler->handle($ticket, new ReplaceMotiveCommand($motive)); @@ -134,10 +167,15 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase Argument::that(fn (ChangeEmergencyStateCommand $command) => EmergencyStatusEnum::YES === $command->newEmergencyStatus) )->shouldBeCalled(); + // Expect event dispatch for motive update + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Argument::type(MotiveUpdateEvent::class), TicketUpdateEvent::class)->shouldBeCalled(); + // Create the handler with our mocks $handler = $this->buildHandler( $entityManager->reveal(), - $changeEmergencyStateCommandHandler->reveal() + $changeEmergencyStateCommandHandler->reveal(), + $eventDispatcher->reveal() ); // Handle the command @@ -166,10 +204,15 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase Argument::cetera() )->shouldNotBeCalled(); + // Expect event dispatch for motive update + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Argument::type(MotiveUpdateEvent::class), TicketUpdateEvent::class)->shouldBeCalled(); + // Create the handler with our mocks $handler = $this->buildHandler( $entityManager->reveal(), - $changeEmergencyStateCommandHandler->reveal() + $changeEmergencyStateCommandHandler->reveal(), + $eventDispatcher->reveal() ); // Handle the command