From e17203ca3abc14e3358341bb6cfde78ea93a81ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 4 Sep 2024 13:48:46 +0200 Subject: [PATCH] Add PDF signature zone availability checks Introduce `PDFSignatureZoneAvailable` service to check available PDF signature zones. Updated `WorkflowAddSignatureController` to use the new service. Added unit tests to verify the correctness of the functionality. --- .../Signature/PDFSignatureZoneAvailable.php | 69 +++++++++++++++++ .../PDFSignatureZoneAvailableTest.php | 76 +++++++++++++++++++ .../WorkflowAddSignatureController.php | 37 ++++----- .../Entity/Workflow/EntityWorkflow.php | 7 +- .../WorkflowAddSignatureControllerTest.php | 72 ++++++++++++++++++ 5 files changed, 236 insertions(+), 25 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneAvailable.php create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneAvailableTest.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Controller/WorkflowAddSignatureControllerTest.php diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneAvailable.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneAvailable.php new file mode 100644 index 000000000..9a7fe8437 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneAvailable.php @@ -0,0 +1,69 @@ + + */ + public function getAvailableSignatureZones(EntityWorkflow $entityWorkflow): array + { + $storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow); + + if (null === $storedObject) { + throw new \RuntimeException('No stored object found'); + } + + if ('application/pdf' !== $storedObject->getType()) { + throw new \RuntimeException('Only PDF documents are supported'); + } + + $zones = $this->pdfSignatureZoneParser->findSignatureZones($this->storedObjectManager->read($storedObject)); + $signatureZonesIndexes = array_map( + fn (EntityWorkflowStepSignature $step) => $step->getZoneSignatureIndex(), + $this->collectSignaturesInUse($entityWorkflow) + ); + + return array_filter($zones, fn (PDFSignatureZone $zone) => !in_array($zone->index, $signatureZonesIndexes, true)); + } + + /** + * @return list + */ + private function collectSignaturesInUse(EntityWorkflow $entityWorkflow): array + { + return array_reduce($entityWorkflow->getSteps()->toArray(), function (array $result, EntityWorkflowStep $step) { + $current = [...$result]; + foreach ($step->getSignatures() as $signature) { + if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) { + $current[] = $signature; + } + } + + return $current; + }, []); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneAvailableTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneAvailableTest.php new file mode 100644 index 000000000..ecc96bf89 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneAvailableTest.php @@ -0,0 +1,76 @@ +registerVersion(type: 'application/pdf'); + + $entityWorkflow = new EntityWorkflow(); + $dto1 = new WorkflowTransitionContextDTO($entityWorkflow); + $dto1->futurePersonSignatures[] = new Person(); + $entityWorkflow->setStep('step1', $dto1, 'transition1', $clock->now()); + $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first(); + $signature->setZoneSignatureIndex(1)->setState(EntityWorkflowSignatureStateEnum::SIGNED); + + $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); + $entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($storedObject); + + $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); + $storedObjectManager->read($storedObject)->willReturn('fake-content'); + + $parser = $this->prophesize(PDFSignatureZoneParser::class); + $parser->findSignatureZones('fake-content')->willReturn([ + $zone1 = new PDFSignatureZone(1, 0.0, 10.0, 20.0, 20.0, new PDFPage(1, 500, 500)), + $zone2 = new PDFSignatureZone(2, 0.0, 10.0, 20.0, 20.0, new PDFPage(1, 500, 500)), + ]); + + $filter = new PDFSignatureZoneAvailable( + $entityWorkflowManager->reveal(), + $parser->reveal(), + $storedObjectManager->reveal(), + ); + + $actual = $filter->getAvailableSignatureZones($entityWorkflow); + + self::assertNotContains($zone1, $actual); + self::assertContains($zone2, $actual); + self::assertCount(1, $actual, 'there should be only one remaining zone'); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowAddSignatureController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowAddSignatureController.php index 9575aa9c9..5024f6684 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowAddSignatureController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowAddSignatureController.php @@ -11,11 +11,9 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; -use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser; -use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; +use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; use Chill\MainBundle\Workflow\EntityWorkflowManager; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -23,21 +21,18 @@ use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Twig\Environment; -final class WorkflowAddSignatureController +final readonly class WorkflowAddSignatureController { public function __construct( - private readonly EntityManagerInterface $entityManager, - private readonly EntityWorkflowManager $entityWorkflowManager, - private readonly StoredObjectManagerInterface $storedObjectManager, - private readonly PDFSignatureZoneParser $PDFSignatureZoneParser, - private readonly NormalizerInterface $normalizer, - private readonly Environment $twig + private EntityWorkflowManager $entityWorkflowManager, + private PDFSignatureZoneAvailable $PDFSignatureZoneAvailable, + private NormalizerInterface $normalizer, + private Environment $twig ) {} - #[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/sign', name: 'chill_main_workflow_signature', methods: 'GET')] - public function __invoke(int $signature_id, Request $request, WorkflowController $workflowController): Response + #[Route(path: '/{_locale}/main/workflow/signature/{id}/sign', name: 'chill_main_workflow_signature', methods: 'GET')] + public function __invoke(EntityWorkflowStepSignature $signature, Request $request): Response { - $signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id); $entityWorkflow = $signature->getStep()->getEntityWorkflow(); $storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow); @@ -45,20 +40,18 @@ final class WorkflowAddSignatureController throw new NotFoundHttpException('No stored object found'); } - $zones = []; - $content = $this->storedObjectManager->read($storedObject); - if (null != $content) { - $zones = $this->PDFSignatureZoneParser->findSignatureZones($content); - } + $zones = $this->PDFSignatureZoneAvailable->getAvailableSignatureZones($entityWorkflow); $signatureClient = []; $signatureClient['id'] = $signature->getId(); $signatureClient['storedObject'] = $this->normalizer->normalize($storedObject, 'json'); $signatureClient['zones'] = $zones; - return new Response($this->twig->render( - '@ChillMain/Workflow/_signature_sign.html.twig', - ['signature' => $signatureClient] - )); + return new Response( + $this->twig->render( + '@ChillMain/Workflow/_signature_sign.html.twig', + ['signature' => $signatureClient] + ) + ); } } diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index 4ab1ee30e..e653037e0 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -20,6 +20,7 @@ use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation; use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\Selectable; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Validator\Constraints as Assert; @@ -52,12 +53,12 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface private int $relatedEntityId; /** - * @var Collection + * @var Collection&Selectable */ #[Assert\Valid(traverse: true)] #[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowStep::class, cascade: ['persist'], orphanRemoval: true)] #[ORM\OrderBy(['transitionAt' => \Doctrine\Common\Collections\Criteria::ASC, 'id' => 'ASC'])] - private Collection $steps; + private Collection&Selectable $steps; /** * @var array|EntityWorkflowStep[]|null @@ -242,7 +243,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface throw new \RuntimeException(); } - public function getSteps(): ArrayCollection|Collection + public function getSteps(): Collection&Selectable { return $this->steps; } diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/WorkflowAddSignatureControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/WorkflowAddSignatureControllerTest.php new file mode 100644 index 000000000..81c8d095e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/WorkflowAddSignatureControllerTest.php @@ -0,0 +1,72 @@ +futurePersonSignatures[] = new Person(); + $entityWorkflow->setStep('step_signature', $stepTransition, 'to_signature', new \DateTimeImmutable('now'), new User()); + + $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first(); + + $entityWorkflowManager = $this->createMock(EntityWorkflowManager::class); + $entityWorkflowManager->method('getAssociatedStoredObject') + ->with($entityWorkflow) + ->willReturn($storedObject); + + $pdfSignatureZoneAvailable = $this->createMock(PDFSignatureZoneAvailable::class); + $pdfSignatureZoneAvailable->method('getAvailableSignatureZones')->withAnyParameters() + ->willReturn([ + new PDFSignatureZone(1, 0.0, 0.0, 100, 100, new PDFPage(1, 500.0, 500.0)), + ]); + + $normalizer = $this->createMock(NormalizerInterface::class); + $normalizer->method('normalize')->withAnyParameters() + ->willReturn([]); + + $twig = $this->createMock(Environment::class); + $twig->method('render')->with('@ChillMain/Workflow/_signature_sign.html.twig', $this->isType('array')) + ->willReturn('ok'); + + $controller = new WorkflowAddSignatureController($entityWorkflowManager, $pdfSignatureZoneAvailable, $normalizer, $twig); + + $actual = $controller($signature, new Request()); + + self::assertEquals(200, $actual->getStatusCode()); + self::assertEquals('ok', $actual->getContent()); + } +}