From 1197a46f5f934d4ffb9c1cf449c5e47f9de6c542 Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 18 Jul 2024 17:13:47 +0200 Subject: [PATCH] Refactor PDF signature handling and add signature state changer Simplified PdfSignedMessageHandler by delegating signature state changes to a new SignatureStepStateChanger class. Added utility method to EntityWorkflowStepSignature for checking pending signatures and created new test cases for the SignatureStepStateChanger. --- .../BaseSigner/PdfSignedMessageHandler.php | 9 +- .../PdfSignedMessageHandlerTest.php | 10 +- .../Workflow/EntityWorkflowStepSignature.php | 26 ++++ .../SignatureStepStateChangerTest.php | 145 ++++++++++++++++++ .../Workflow/SignatureStepStateChanger.php | 96 ++++++++++++ 5 files changed, 276 insertions(+), 10 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Tests/Workflow/SignatureStepStateChangerTest.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/SignatureStepStateChanger.php diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php index 720bc000f..e97781a15 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php @@ -12,12 +12,11 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; -use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository; use Chill\MainBundle\Workflow\EntityWorkflowManager; +use Chill\MainBundle\Workflow\SignatureStepStateChanger; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; -use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; final readonly class PdfSignedMessageHandler implements MessageHandlerInterface @@ -33,7 +32,7 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface private StoredObjectManagerInterface $storedObjectManager, private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository, private EntityManagerInterface $entityManager, - private ClockInterface $clock, + private SignatureStepStateChanger $signatureStepStateChanger, ) {} public function __invoke(PdfSignedMessage $message): void @@ -54,8 +53,8 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface $this->storedObjectManager->write($storedObject, $message->content); - $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate($this->clock->now()); - $signature->setZoneSignatureIndex($message->signatureZoneIndex); + $this->signatureStepStateChanger->markSignatureAsSigned($signature, $message->signatureZoneIndex); + $this->entityManager->flush(); $this->entityManager->clear(); } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandlerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandlerTest.php index 471dc8f9a..fed1b1274 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandlerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandlerTest.php @@ -20,12 +20,12 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository; use Chill\MainBundle\Workflow\EntityWorkflowManager; +use Chill\MainBundle\Workflow\SignatureStepStateChanger; use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Chill\PersonBundle\Entity\Person; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; -use Symfony\Component\Clock\MockClock; /** * @internal @@ -45,6 +45,9 @@ class PdfSignedMessageHandlerTest extends TestCase $entityWorkflow->setStep('new_step', $dto, 'new_transition', new \DateTimeImmutable(), new User()); $step = $entityWorkflow->getCurrentStep(); $signature = $step->getSignatures()->first(); + $stateChanger = $this->createMock(SignatureStepStateChanger::class); + $stateChanger->expects(self::once())->method('markSignatureAsSigned') + ->with($signature, 99); $handler = new PdfSignedMessageHandler( new NullLogger(), @@ -52,15 +55,12 @@ class PdfSignedMessageHandlerTest extends TestCase $this->buildStoredObjectManager($storedObject, $expectedContent = '1234'), $this->buildSignatureRepository($signature), $this->buildEntityManager(true), - new MockClock('now'), + $stateChanger, ); // we simply call the handler. The mocked StoredObjectManager will check that the "write" method is invoked once // with the content "1234" $handler(new PdfSignedMessage(10, 99, $expectedContent)); - - self::assertEquals('signed', $signature->getState()->value); - self::assertEquals(99, $signature->getZoneSignatureIndex()); } private function buildSignatureRepository(EntityWorkflowStepSignature $signature): EntityWorkflowStepSignatureRepository diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php index 7f6c5cdda..a0fe1755c 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php @@ -141,6 +141,32 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate return EntityWorkflowSignatureStateEnum::SIGNED == $this->getState(); } + public function isPending(): bool + { + return EntityWorkflowSignatureStateEnum::PENDING == $this->getState(); + } + + /** + * Checks whether all signatures associated with a given workflow step are not pending. + * + * Iterates over each signature in the provided workflow step, and returns false if any signature + * is found to be pending. If all signatures are not pending, returns true. + * + * @param EntityWorkflowStep $step the workflow step whose signatures are to be checked + * + * @return bool true if all signatures are not pending, false otherwise + */ + public static function isAllSignatureNotPendingForStep(EntityWorkflowStep $step): bool + { + foreach ($step->getSignatures() as $signature) { + if ($signature->isPending()) { + return false; + } + } + + return true; + } + /** * @return 'person'|'user' */ diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/SignatureStepStateChangerTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/SignatureStepStateChangerTest.php new file mode 100644 index 000000000..5eb82bab9 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/SignatureStepStateChangerTest.php @@ -0,0 +1,145 @@ +setWorkflowName('dummy'); + $registry = $this->buildRegistry(); + $workflow = $registry->get($entityWorkflow, 'dummy'); + $clock = new MockClock(); + $user = new User(); + $changer = new SignatureStepStateChanger($registry, $clock); + + // move it to signature + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futurePersonSignatures = [new Person(), new Person()]; + $workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transitionAt' => $clock->now(), + 'byUser' => $user, 'transition' => 'to_signature']); + + // get the signature created + $signatures = $entityWorkflow->getCurrentStep()->getSignatures(); + + if (2 !== count($signatures)) { + throw new \LogicException('there should have 2 signatures at this step'); + } + + // we mark the first signature as signed + $changer->markSignatureAsSigned($signatures[0], 1); + + self::assertEquals('signature', $entityWorkflow->getStep(), 'there should have any change in the entity workflow step'); + self::assertEquals(EntityWorkflowSignatureStateEnum::SIGNED, $signatures[0]->getState()); + self::assertEquals(1, $signatures[0]->getZoneSignatureIndex()); + self::assertNotNull($signatures[0]->getStateDate()); + + + // we mark the second signature as signed + $changer->markSignatureAsSigned($signatures[1], 2); + self::assertEquals(EntityWorkflowSignatureStateEnum::SIGNED, $signatures[1]->getState()); + self::assertEquals('post-signature', $entityWorkflow->getStep(), 'the entity workflow step should be post-signature'); + self::assertContains($user, $entityWorkflow->getCurrentStep()->getAllDestUser()); + self::assertEquals(2, $signatures[1]->getZoneSignatureIndex()); + self::assertNotNull($signatures[1]->getStateDate()); + } + + public function testMarkSignatureAsSignedScenarioWithoutRequiredMetadata() + { + $entityWorkflow = new EntityWorkflow(); + $entityWorkflow->setWorkflowName('dummy'); + $registry = $this->buildRegistry(); + $workflow = $registry->get($entityWorkflow, 'dummy'); + $clock = new MockClock(); + $user = new User(); + $changer = new SignatureStepStateChanger($registry, $clock); + + // move it to signature + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futurePersonSignatures = [new Person()]; + $workflow->apply($entityWorkflow, 'to_signature-without-metadata', ['context' => $dto, 'transitionAt' => $clock->now(), + 'byUser' => $user, 'transition' => 'to_signature-without-metadata']); + + // get the signature created + $signatures = $entityWorkflow->getCurrentStep()->getSignatures(); + + if (1 !== count($signatures)) { + throw new \LogicException('there should have 2 signatures at this step'); + } + + // we mark the first signature as signed + $changer->markSignatureAsSigned($signatures[0], 1); + + self::assertEquals('signature-without-metadata', $entityWorkflow->getStep(), 'there should have any change in the entity workflow step'); + self::assertEquals(EntityWorkflowSignatureStateEnum::SIGNED, $signatures[0]->getState()); + self::assertEquals(1, $signatures[0]->getZoneSignatureIndex()); + self::assertNotNull($signatures[0]->getStateDate()); + } + + private function buildRegistry(): Registry + { + $builder = new DefinitionBuilder(); + $builder + ->setInitialPlaces('initial') + ->addPlaces(['initial', 'signature', 'signature-without-metadata', 'post-signature']) + ->addTransition(new Transition('to_signature', 'initial', 'signature')) + ->addTransition(new Transition('to_signature-without-metadata', 'initial', 'signature-without-metadata')) + ->addTransition(new Transition('to_post-signature', 'signature', 'post-signature')) + ->addTransition(new Transition('to_post-signature_2', 'signature-without-metadata', 'post-signature')) + ; + + $metadata = new InMemoryMetadataStore( + [], + [ + 'signature' => ['onSignatureCompleted' => ['transitionName' => 'to_post-signature']], + ] + ); + $builder->setMetadataStore($metadata); + + $workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy'); + $registry = new Registry(); + $registry->addWorkflow( + $workflow, + new class () implements WorkflowSupportStrategyInterface { + public function supports(WorkflowInterface $workflow, object $subject): bool + { + return true; + } + } + ); + + return $registry; + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/SignatureStepStateChanger.php b/src/Bundle/ChillMainBundle/Workflow/SignatureStepStateChanger.php new file mode 100644 index 000000000..efba00b02 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/SignatureStepStateChanger.php @@ -0,0 +1,96 @@ +setState(EntityWorkflowSignatureStateEnum::SIGNED) + ->setZoneSignatureIndex($atIndex) + ->setStateDate($this->clock->now()) + ; + + if (!EntityWorkflowStepSignature::isAllSignatureNotPendingForStep($signature->getStep())) { + return; + } + + $entityWorkflow = $signature->getStep()->getEntityWorkflow(); + $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); + $metadataStore = $workflow->getMetadataStore(); + + // find a transition + $marking = $workflow->getMarking($entityWorkflow); + $places = $marking->getPlaces(); + + $transition = null; + foreach ($places as $place => $int) { + $metadata = $metadataStore->getPlaceMetadata($place); + if (array_key_exists('onSignatureCompleted', $metadata)) { + $transition = $metadata['onSignatureCompleted']['transitionName']; + } + } + + if (null === $transition) { + return; + } + + $previousUser = $this->getPreviousSender($signature->getStep()); + + if (null === $previousUser) { + return; + } + + $transitionDto = new WorkflowTransitionContextDTO($entityWorkflow); + $transitionDto->futureDestUsers[] = $previousUser; + + $workflow->apply($entityWorkflow, $transition, [ + 'context' => $transitionDto, + 'transitionAt' => $this->clock->now(), + 'transition' => $transition, + ]); + } + + private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User + { + $stepsChained = $entityWorkflowStep->getEntityWorkflow()->getStepsChained(); + + foreach ($stepsChained as $stepChained) { + if ($stepChained === $entityWorkflowStep) { + if (null === $previous = $stepChained->getPrevious()) { + return null; + } + + if (null !== $previousUser = $previous->getTransitionBy()) { + return $previousUser; + } + + return $this->getPreviousSender($previous); + } + } + + throw new \LogicException('no same step found'); + } +}