From 3db4fff80df6ce82c97b8c6b00b1cd037a3ccb66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 28 Jun 2024 11:58:39 +0200 Subject: [PATCH] Add signature functionality to workflow entities Created new files to add signature functionality to the workflow entities, including signature state enums and signature metadata. Added these changes to the migration script as well. Updated EntityWorkflowStep to include a collection for signatures. --- .../EntityWorkflowSignatureStateEnum.php | 20 ++ .../Entity/Workflow/EntityWorkflowStep.php | 42 ++- .../Workflow/EntityWorkflowStepSignature.php | 156 ++++++++++ .../EntityWorkflowStepSignatureRepository.php | 54 ++++ .../EntityWorkflowStepSignatureTest.php | 69 ++++ .../migrations/Version20240628095159.php | 51 +++ tests/app/config/packages/workflow_chill.yaml | 294 ++++++++++++++++++ 7 files changed, 684 insertions(+), 2 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php create mode 100644 src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php create mode 100644 src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20240628095159.php create mode 100644 tests/app/config/packages/workflow_chill.yaml diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php new file mode 100644 index 000000000..2d3e4269f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php @@ -0,0 +1,20 @@ + + * @var Collection */ #[ORM\ManyToMany(targetEntity: User::class)] #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')] private Collection $destUser; /** - * @var Collection + * @var Collection */ #[ORM\ManyToMany(targetEntity: User::class)] #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')] private Collection $destUserByAccessKey; + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)] + private Collection $signatures; + #[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')] private ?EntityWorkflow $entityWorkflow = null; @@ -97,6 +103,7 @@ class EntityWorkflowStep $this->ccUser = new ArrayCollection(); $this->destUser = new ArrayCollection(); $this->destUserByAccessKey = new ArrayCollection(); + $this->signatures = new ArrayCollection(); $this->accessKey = bin2hex(openssl_random_pseudo_bytes(32)); } @@ -136,6 +143,29 @@ class EntityWorkflowStep return $this; } + /** + * @internal use @see{EntityWorkflowStepSignature}'s constructor instead + */ + public function addSignature(EntityWorkflowStepSignature $signature): self + { + if (!$this->signatures->contains($signature)) { + $this->signatures[] = $signature; + } + + return $this; + } + + public function removeSignature(EntityWorkflowStepSignature $signature): self + { + if ($this->signatures->contains($signature)) { + $this->signatures->removeElement($signature); + } + + $signature->detachEntityWorkflowStep(); + + return $this; + } + public function getAccessKey(): string { return $this->accessKey; @@ -198,6 +228,14 @@ class EntityWorkflowStep return $this->entityWorkflow; } + /** + * @return Collection + */ + public function getSignatures(): Collection + { + return $this->signatures; + } + public function getId(): ?int { return $this->id; diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php new file mode 100644 index 000000000..25babc3c6 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php @@ -0,0 +1,156 @@ + null])] + private ?\DateTimeImmutable $stateDate = null; + + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])] + private array $signatureMetadata = []; + + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])] + private ?int $zoneSignatureIndex = null; + + #[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')] + #[ORM\JoinColumn(nullable: false)] + private ?EntityWorkflowStep $step = null; + + public function __construct( + EntityWorkflowStep $step, + User|Person $signer, + ) { + $this->step = $step; + $step->addSignature($this); + $this->setSigner($signer); + } + + private function setSigner(User|Person $signer): void + { + if ($signer instanceof User) { + $this->userSigner = $signer; + } else { + $this->personSigner = $signer; + } + } + + public function getId(): ?int + { + return $this->id; + } + + public function getStep(): EntityWorkflowStep + { + return $this->step; + } + + public function getSigner(): User|Person + { + if (null !== $this->userSigner) { + return $this->userSigner; + } + + return $this->personSigner; + } + + public function getSignatureMetadata(): array + { + return $this->signatureMetadata; + } + + public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature + { + $this->signatureMetadata = $signatureMetadata; + + return $this; + } + + public function getState(): EntityWorkflowSignatureStateEnum + { + return $this->state; + } + + public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature + { + $this->state = $state; + + return $this; + } + + public function getStateDate(): ?\DateTimeImmutable + { + return $this->stateDate; + } + + public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature + { + $this->stateDate = $stateDate; + + return $this; + } + + public function getZoneSignatureIndex(): ?int + { + return $this->zoneSignatureIndex; + } + + public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature + { + $this->zoneSignatureIndex = $zoneSignatureIndex; + + return $this; + } + + /** + * Detach from the @see{EntityWorkflowStep}. + * + * @internal used internally to remove the current signature + * + * @return $this + */ + public function detachEntityWorkflowStep(): self + { + $this->step = null; + + return $this; + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php new file mode 100644 index 000000000..0e1393242 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php @@ -0,0 +1,54 @@ + + */ +class EntityWorkflowStepSignatureRepository implements ObjectRepository +{ + private \Doctrine\ORM\EntityRepository $repository; + + public function __construct(EntityManagerInterface $entityManager) + { + $this->repository = $entityManager->getRepository(EntityWorkflowStepSignature::class); + } + + public function find($id): ?EntityWorkflowStepSignature + { + return $this->repository->find($id); + } + + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?EntityWorkflowStepSignature + { + return $this->findOneBy($criteria); + } + + public function getClassName(): string + { + return EntityWorkflowStepSignature::class; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php new file mode 100644 index 000000000..62b6a7d6a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php @@ -0,0 +1,69 @@ +entityManager = self::getContainer()->get(EntityManagerInterface::class); + } + + public function testConstruct() + { + $workflow = new EntityWorkflow(); + $workflow->setWorkflowName('vendee_internal') + ->setRelatedEntityId(0) + ->setRelatedEntityClass(AccompanyingPeriodWorkEvaluationDocument::class); + + $step = $workflow->getCurrentStep(); + + $person = $this->entityManager->createQuery('SELECT p FROM '.Person::class.' p') + ->setMaxResults(1) + ->getSingleResult(); + + $signature = new EntityWorkflowStepSignature($step, $person); + + self::assertCount(1, $step->getSignatures()); + self::assertSame($signature, $step->getSignatures()->first()); + + $this->entityManager->getConnection()->beginTransaction(); + $this->entityManager->persist($workflow); + $this->entityManager->persist($step); + $this->entityManager->persist($signature); + + $this->entityManager->flush(); + $this->entityManager->getConnection()->commit(); + + $this->entityManager->clear(); + + $signatureBis = $this->entityManager->find(EntityWorkflowStepSignature::class, $signature->getId()); + + self::assertEquals($signature->getId(), $signatureBis->getId()); + self::assertEquals($step->getId(), $signatureBis->getStep()->getId()); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20240628095159.php b/src/Bundle/ChillMainBundle/migrations/Version20240628095159.php new file mode 100644 index 000000000..4452530c6 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20240628095159.php @@ -0,0 +1,51 @@ +addSql('CREATE SEQUENCE chill_main_workflow_entity_step_signature_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_workflow_entity_step_signature (id INT NOT NULL, step_id INT NOT NULL, '. + 'state VARCHAR(50) NOT NULL, stateDate TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, signatureMetadata JSON DEFAULT \'[]\' NOT NULL,'. + ' zoneSignatureIndex INT DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,'. + ' userSigner_id INT DEFAULT NULL, personSigner_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_C47D4BA3D934E3A4 ON chill_main_workflow_entity_step_signature (userSigner_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA3ADFFA293 ON chill_main_workflow_entity_step_signature (personSigner_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA373B21E9C ON chill_main_workflow_entity_step_signature (step_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA33174800F ON chill_main_workflow_entity_step_signature (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA365FF1AEC ON chill_main_workflow_entity_step_signature (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.stateDate IS \'(DC2Type:datetimetz_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3D934E3A4 FOREIGN KEY (userSigner_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3ADFFA293 FOREIGN KEY (personSigner_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA373B21E9C FOREIGN KEY (step_id) REFERENCES chill_main_workflow_entity_step (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA33174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA365FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP SEQUENCE chill_main_workflow_entity_step_signature_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_main_workflow_entity_step_signature'); + } +} diff --git a/tests/app/config/packages/workflow_chill.yaml b/tests/app/config/packages/workflow_chill.yaml new file mode 100644 index 000000000..82662461b --- /dev/null +++ b/tests/app/config/packages/workflow_chill.yaml @@ -0,0 +1,294 @@ +framework: + workflows: + vendee_internal: + type: state_machine + metadata: + related_entity: + - Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument + - Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork + - Chill\DocStoreBundle\Entity\AccompanyingCourseDocument + label: + fr: 'Suivi' + support_strategy: Chill\MainBundle\Workflow\RelatedEntityWorkflowSupportsStrategy + initial_marking: 'initial' + marking_store: + property: step + type: method + places: + initial: + metadata: + label: + fr: Étape initiale + attenteModification: + metadata: + label: + fr: En attente de modification du document + validationFilterInputLabels: + forward: {fr: Modification effectuée} + backward: {fr: Pas de modification effectuée} + neutral: {fr: Autre} + attenteMiseEnForme: + metadata: + label: + fr: En attente de mise en forme + validationFilterInputLabels: + forward: {fr: Mise en forme terminée} + backward: {fr: Pas de mise en forme effectuée} + neutral: {fr: Autre} + attenteVisa: + metadata: + label: + fr: En attente de visa + validationFilterInputLabels: + forward: {fr: Visa accordé} + backward: {fr: Visa refusé} + neutral: {fr: Autre} + attenteSignature: + metadata: + label: + fr: En attente de signature + validationFilterInputLabels: + forward: {fr: Signature accordée} + backward: {fr: Signature refusée} + neutral: {fr: Autre} + attenteTraitement: + metadata: + label: + fr: En attente de traitement + validationFilterInputLabels: + forward: {fr: Traitement terminé favorablement} + backward: {fr: Traitement terminé défavorablement} + neutral: {fr: Autre} + attenteEnvoi: + metadata: + label: + fr: En attente d'envoi + validationFilterInputLabels: + forward: {fr: Document envoyé} + backward: {fr: Document non envoyé} + neutral: {fr: Autre} + attenteValidationMiseEnForme: + metadata: + label: + fr: En attente de validation de la mise en forme + validationFilterInputLabels: + forward: {fr: Validation de la mise en forme} + backward: {fr: Refus de validation de la mise en forme} + neutral: {fr: Autre} + annule: + metadata: + isFinal: true + isFinalPositive: false + label: + fr: Annulé + final: + metadata: + isFinal: true + isFinalPositive: true + label: + fr: Finalisé + transitions: + # transition qui avancent + demandeModificationDocument: + from: + - initial + to: attenteModification + metadata: + label: + fr: Demande de modification du document + isForward: true + demandeMiseEnForme: + from: + - initial + - attenteModification + to: attenteMiseEnForme + metadata: + label: + fr: Demande de mise en forme + isForward: true + demandeValidationMiseEnForme: + from: + - attenteMiseEnForme + to: attenteValidationMiseEnForme + metadata: + label: + fr: Demande de validation de la mise en forme + isForward: true + demandeVisa: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + to: attenteVisa + metadata: + label: + fr: Demande de visa + isForward: true + demandeSignature: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + to: attenteSignature + metadata: + label: {fr: Demande de signature} + isForward: true + demandeTraitement: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + - attenteSignature + to: attenteTraitement + metadata: + label: {fr: Demande de traitement} + isForward: true + demandeEnvoi: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + - attenteSignature + - attenteTraitement + to: attenteEnvoi + metadata: + label: {fr: Demande d'envoi} + isForward: true + annulation: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: annule + metadata: + label: {fr: Annulation} + isForward: false + # transitions qui répètent l'étape + demandeMiseEnFormeSupplementaire: + from: + - attenteMiseEnForme + - attenteValidationMiseEnForme + to: attenteMiseEnForme + metadata: + label: {fr: Demande de mise en forme supplémentaire} + demandeVisaSupplementaire: + from: + - attenteVisa + to: attenteVisa + metadata: + label: {fr: Demande de visa supplémentaire} + isForward: true + demandeSignatureSupplementaire: + from: + - attenteSignature + to: attenteSignature + metadata: + label: {fr: Demande de signature supplémentaire} + demandeTraitementSupplementaire: + from: + - attenteTraitement + to: attenteTraitement + metadata: + label: {fr: Demande de traitement supplémentaire} + # transitions qui renvoient vers une étape précédente + refusEtModificationDocument: + from: + - attenteVisa + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: attenteModification + metadata: + label: + fr: Refus et demande de modification du document + isForward: false + refusEtDemandeMiseEnForme: + from: + - attenteVisa + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: attenteMiseEnForme + metadata: + label: {fr: Refus et demande de mise en forme} + isForward: false + refusEtDemandeVisa: + from: + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: attenteVisa + metadata: + label: {fr: Refus et demande de visa} + isForward: false + refusEtDemandeSignature: + from: + - attenteTraitement + - attenteEnvoi + to: attenteSignature + metadata: + label: {fr: Refus et demande de signature} + isForward: false + refusEtDemandeTraitement: + from: + - attenteEnvoi + to: attenteTraitement + metadata: + label: {fr: Refus et demande de traitement} + isForward: false + # transition vers final + initialToFinal: + from: + - initial + to: final + metadata: + label: {fr: Clotûre immédiate et cloture positive} + isForward: true + attenteMiseEnFormeToFinal: + from: + - attenteMiseEnForme + - attenteValidationMiseEnForme + to: final + metadata: + label: {fr: Mise en forme terminée et cloture positive} + isForward: true + attenteVisaToFinal: + from: + - attenteVisa + to: final + metadata: + label: {fr: Accorde le visa et cloture positive} + isForward: true + attenteSignatureToFinal: + from: + - attenteSignature + to: final + metadata: + label: {fr: Accorde la signature et cloture positive} + isForward: true + attenteTraitementToFinal: + from: + - attenteTraitement + to: final + metadata: + label: {fr: Traitement terminé et cloture postive} + isForward: true + attenteEnvoiToFinal: + from: + - attenteEnvoi + to: final + metadata: + label: {fr: Envoyé et cloture postive} + isForward: true