Signature fixes

This commit is contained in:
2025-10-03 09:49:33 +00:00
committed by Julien Fastré
parent c8bb7575e7
commit be39fa16e7
40 changed files with 1393 additions and 123 deletions

View File

@@ -11,9 +11,12 @@ declare(strict_types=1);
namespace Chill\MainBundle\Workflow\Helper;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Security\Core\Security;
@@ -58,21 +61,39 @@ class WorkflowRelatedEntityPermissionHelper
public function __construct(
private readonly Security $security,
private readonly EntityWorkflowManager $entityWorkflowManager,
private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
private readonly Registry $registry,
private readonly ClockInterface $clock,
) {}
/**
* @param object $entity The entity may be an
*
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
*/
public function isAllowedByWorkflowForReadOperation(object $entity): string
{
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
if ($entity instanceof StoredObject) {
$attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
$entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
$isAttached = true;
} else {
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
$isAttached = false;
}
if ([] === $entityWorkflows) {
return self::ABSTAIN;
}
if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) {
return self::FORCE_GRANT;
}
if ($isAttached) {
return self::ABSTAIN;
}
// give a view permission if there is a Person signature pending, or in the 12 hours following
// the signature last state
foreach ($entityWorkflows as $workflow) {
@@ -100,33 +121,51 @@ class WorkflowRelatedEntityPermissionHelper
*/
public function isAllowedByWorkflowForWriteOperation(object $entity): string
{
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
$runningWorkflows = [];
if ($entity instanceof StoredObject) {
$attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
$entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
$isAttached = true;
} else {
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
$isAttached = false;
}
// if a workflow is finalized positive, we are not allowed to edit to document any more
if ([] === $entityWorkflows) {
return self::ABSTAIN;
}
// if a workflow is finalized positive, anyone is allowed to edit the document anymore
foreach ($entityWorkflows as $entityWorkflow) {
if ($entityWorkflow->isFinal()) {
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
foreach ($marking->getPlaces() as $place => $int) {
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
if (true === ($placeMetadata['isFinalPositive'] ?? false)) {
// the workflow is final, and final positive, so we stop here.
return self::FORCE_DENIED;
}
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
foreach ($marking->getPlaces() as $place => $int) {
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
if (
($entityWorkflow->isFinal() && ($placeMetadata['isFinalPositive'] ?? false))
|| ($placeMetadata['isSentExternal'] ?? false)
) {
// the workflow is final, and final positive, or is sentExternal, so we stop here.
return self::FORCE_DENIED;
}
if (
// if not finalized positive
$entityWorkflow->isFinal() && !($placeMetadata['isFinalPositive'] ?? false)
) {
return self::ABSTAIN;
}
} else {
$runningWorkflows[] = $entityWorkflow;
}
}
// if there is a signature on a **running workflow**, no one can edit the workflow any more
foreach ($runningWorkflows as $entityWorkflow) {
foreach ($entityWorkflow->getSteps() as $step) {
foreach ($step->getSignatures() as $signature) {
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
return self::FORCE_DENIED;
$runningWorkflows = array_filter($entityWorkflows, fn (EntityWorkflow $ew) => !$ew->isFinal());
// if there is a signature on a **running workflow**, no one is allowed edit the workflow anymore
if (!$isAttached) {
foreach ($runningWorkflows as $entityWorkflow) {
foreach ($entityWorkflow->getSteps() as $step) {
foreach ($step->getSignatures() as $signature) {
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
return self::FORCE_DENIED;
}
}
}
}
@@ -137,7 +176,11 @@ class WorkflowRelatedEntityPermissionHelper
return self::FORCE_GRANT;
}
return self::ABSTAIN;
if ($isAttached) {
return self::ABSTAIN;
}
return self::FORCE_DENIED;
}
/**

View File

@@ -11,9 +11,13 @@ declare(strict_types=1);
namespace Chill\MainBundle\Workflow\Messenger;
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
@@ -25,6 +29,8 @@ final readonly class PostSendExternalMessageHandler implements MessageHandlerInt
private EntityWorkflowRepository $entityWorkflowRepository,
private MailerInterface $mailer,
private EntityWorkflowManager $workflowManager,
private StoredObjectToPdfConverter $storedObjectToPdfConverter,
private LoggerInterface $logger,
) {}
public function __invoke(PostSendExternalMessage $message): void
@@ -35,11 +41,34 @@ final readonly class PostSendExternalMessageHandler implements MessageHandlerInt
throw new UnrecoverableMessageHandlingException(sprintf('Entity workflow with id %d not found', $message->entityWorkflowId));
}
$this->convertToPdf($entityWorkflow, $message->lang);
foreach ($entityWorkflow->getCurrentStep()->getSends() as $send) {
$this->sendEmailToDestinee($send, $message);
}
}
private function convertToPdf(EntityWorkflow $entityWorkflow, string $locale): void
{
foreach ($entityWorkflow->getAttachments() as $attachment) {
try {
$this->storedObjectToPdfConverter->addConvertedVersion($attachment->getProxyStoredObject(), $locale);
} catch (StoredObjectManagerException $e) {
$this->logger->error('Error converting attachment to PDF', ['backtrace' => $e->getTraceAsString(), 'attachment_id' => $attachment->getId()]);
}
}
$storedObject = $this->workflowManager->getAssociatedStoredObject($entityWorkflow);
if (null !== $storedObject) {
try {
$this->storedObjectToPdfConverter->addConvertedVersion($storedObject, $locale);
} catch (StoredObjectManagerException $e) {
$this->logger->error('Error converting stored object to PDF', ['backtrace' => $e->getTraceAsString(), 'stored_object_id' => $storedObject->getId(), 'workflow_id' => $entityWorkflow->getId()]);
}
}
}
private function sendEmailToDestinee(EntityWorkflowSend $send, PostSendExternalMessage $message): void
{
$entityWorkflow = $send->getEntityWorkflowStep()->getEntityWorkflow();

View File

@@ -22,6 +22,7 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\Transition;
/**
* Handles state changes for signature steps within a workflow.
@@ -50,8 +51,10 @@ class SignatureStepStateChanger
*
* @param EntityWorkflowStepSignature $signature the signature entity to be marked as signed
* @param int|null $atIndex optional index position for the signature within the zone
*
* @return string The expected new workflow's step, after transition is applyied
*/
public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): void
public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): string
{
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
@@ -60,7 +63,14 @@ class SignatureStepStateChanger
->setZoneSignatureIndex($atIndex)
->setStateDate($this->clock->now());
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as signed', ['signatureId' => $signature->getId(), 'index' => (string) $atIndex]);
['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
if (null === $transition) {
return $signature->getStep()->getEntityWorkflow()->getStep();
}
return $transition->getTos()[0];
}
/**
@@ -71,8 +81,10 @@ class SignatureStepStateChanger
*
* This method updates the signature state to 'canceled' and logs the action.
* It also dispatches a message to notify about the state change.
*
* @return string The expected new workflow's step, after transition is applyied
*/
public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): void
public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): string
{
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
@@ -80,7 +92,15 @@ class SignatureStepStateChanger
->setState(EntityWorkflowSignatureStateEnum::CANCELED)
->setStateDate($this->clock->now());
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as canceled', ['signatureId' => $signature->getId()]);
['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
if (null === $transition) {
return $signature->getStep()->getEntityWorkflow()->getStep();
}
return $transition->getTos()[0];
}
/**
@@ -93,8 +113,10 @@ class SignatureStepStateChanger
* a state change has occurred.
*
* @param EntityWorkflowStepSignature $signature the signature entity to be marked as rejected
*
* @return string The expected new workflow's step, after transition is applyied
*/
public function markSignatureAsRejected(EntityWorkflowStepSignature $signature): void
public function markSignatureAsRejected(EntityWorkflowStepSignature $signature): string
{
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
@@ -102,7 +124,16 @@ class SignatureStepStateChanger
->setState(EntityWorkflowSignatureStateEnum::REJECTED)
->setStateDate($this->clock->now());
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as rejected', ['signatureId' => $signature->getId()]);
['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
if (null === $transition) {
return $signature->getStep()->getEntityWorkflow()->getStep();
}
return $transition->getTos()[0];
}
/**
@@ -117,10 +148,35 @@ class SignatureStepStateChanger
{
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_READ);
['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
if (null === $transition) {
return;
}
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
$transitionDto->futureDestUsers[] = $futureUser;
$workflow->apply($entityWorkflow, $transition->getName(), [
'context' => $transitionDto,
'transitionAt' => $this->clock->now(),
'transition' => $transition->getName(),
]);
$this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
}
/**
* @return array{transition: Transition|null, futureUser: User|null}
*/
private function decideTransition(EntityWorkflowStepSignature $signature): array
{
if (!EntityWorkflowStepSignature::isAllSignatureNotPendingForStep($signature->getStep())) {
$this->logger->info(self::LOG_PREFIX.'This is not the last signature, skipping transition to another place', ['signatureId' => $signature->getId()]);
return;
return ['transition' => null, 'futureUser' => null];
}
$this->logger->debug(self::LOG_PREFIX.'Continuing the process to find a transition', ['signatureId' => $signature->getId()]);
@@ -144,7 +200,7 @@ class SignatureStepStateChanger
if (null === $transition) {
$this->logger->info(self::LOG_PREFIX.'The transition is not configured, will not apply a transition', ['signatureId' => $signature->getId()]);
return;
return ['transition' => null, 'futureUser' => null];
}
if ('person' === $signature->getSignerKind()) {
@@ -156,19 +212,16 @@ class SignatureStepStateChanger
if (null === $futureUser) {
$this->logger->info(self::LOG_PREFIX.'No previous user, will not apply a transition', ['signatureId' => $signature->getId()]);
return;
return ['transition' => null, 'futureUser' => null];
}
$transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
$transitionDto->futureDestUsers[] = $futureUser;
foreach ($workflow->getDefinition()->getTransitions() as $transitionObj) {
if ($transitionObj->getName() === $transition) {
return ['transition' => $transitionObj, 'futureUser' => $futureUser];
}
}
$workflow->apply($entityWorkflow, $transition, [
'context' => $transitionDto,
'transitionAt' => $this->clock->now(),
'transition' => $transition,
]);
$this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
throw new \RuntimeException('Transition not found');
}
private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User