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