From b5b13326dd37b8c203e8e767a34be44348b23936 Mon Sep 17 00:00:00 2001 From: Carlo Marmo Date: Fri, 16 Oct 2026 16:33:34 +0400 Subject: [PATCH 1/2] fix: apply signer removal to every document in an envelope Keep the signer's identification details available when removing their requests from the documents in an envelope. Add tests for removing one of two signers and removing the last signer. Refs #9085 Signed-off-by: Carlo Marmo --- lib/Service/RequestSignatureService.php | 8 +- .../integration/features/sign/cancel.feature | 102 ++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/lib/Service/RequestSignatureService.php b/lib/Service/RequestSignatureService.php index 65d821dc46..bac7d28546 100644 --- a/lib/Service/RequestSignatureService.php +++ b/lib/Service/RequestSignatureService.php @@ -632,7 +632,7 @@ public function unassociateToUser(int $fileId, int $signRequestId): void { ->setFile($file) ->reorderAfterDeletion($file->getId(), $deletedOrder); - $this->propagateSignerDeletionToChildren($file, $signRequest); + $this->propagateSignerDeletionToChildren($file, $groupedIdentifyMethods); $this->revertStatusToDraftIfNoSignersRemain($file); } catch (\Throwable) { } @@ -647,14 +647,16 @@ private function revertStatusToDraftIfNoSignersRemain(FileEntity $file): void { } } - private function propagateSignerDeletionToChildren(FileEntity $envelope, SignRequestEntity $deletedSignRequest): void { + /** + * @param array> $identifyMethods + */ + private function propagateSignerDeletionToChildren(FileEntity $envelope, array $identifyMethods): void { if ($envelope->getNodeType() !== 'envelope') { return; } $children = $this->fileMapper->getChildrenFiles($envelope->getId()); - $identifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($deletedSignRequest->getId()); if (empty($identifyMethods)) { return; } diff --git a/tests/integration/features/sign/cancel.feature b/tests/integration/features/sign/cancel.feature index 92a8108189..639e930018 100644 --- a/tests/integration/features/sign/cancel.feature +++ b/tests/integration/features/sign/cancel.feature @@ -79,3 +79,105 @@ Feature: sign-request-cancel Then the response should be a JSON array with the following mandatory values | key | value | | (jq).ocs.data.data[0].status | 0 | + + Scenario: Delete envelope signer removes only that signer from both documents + Given as user "admin" + And user "signer1" exists + And user "signer2" exists + And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/request-signature" + | files | [{"base64":"","name":"First.pdf"},{"base64":"","name":"Second.pdf"}] | + | signers | [{"identifyMethods":[{"method":"account","value":"signer1"}]},{"identifyMethods":[{"method":"account","value":"signer2"}]}] | + | name | Envelope cancellation | + | status | 0 | + And the response should have a status code 200 + And fetch field "(ENVELOPE_UUID)ocs.data.uuid" from previous JSON response + And sending "patch" to ocs "/apps/libresign/api/v1/request-signature" + | uuid | | + | signers | [{"identifyMethods":[{"method":"account","value":"signer1"}]},{"identifyMethods":[{"method":"account","value":"signer2"}]}] | + | status | 1 | + And the response should have a status code 200 + And sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" + And the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data.nodeType | envelope | + | (jq).ocs.data.files \| length | 2 | + | (jq)[.ocs.data, .ocs.data.files[]] \| map(.status) | [1,1,1] | + | (jq)[.ocs.data, .ocs.data.files[]] \| map([.signers[].identifyMethods[] \| select(.method == "account").value] \| sort) | [["signer1","signer2"],["signer1","signer2"],["signer1","signer2"]] | + And fetch field "(ENVELOPE_ID)ocs.data.id" from previous JSON response + And fetch field '(SIGN_REQUEST_ID)(jq).ocs.data.signers[] | select(any(.identifyMethods[]; .method == "account" and .value == "signer1")) | .signRequestId' from previous JSON response + And fetch field "(FIRST_UUID)ocs.data.files.0.uuid" from previous JSON response + And fetch field "(SECOND_UUID)ocs.data.files.1.uuid" from previous JSON response + When sending "delete" to ocs "/apps/libresign/api/v1/sign/file_id//" + Then the response should have a status code 200 + When sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" + Then the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data.files \| length | 2 | + | (jq)[.ocs.data, .ocs.data.files[]] \| map(.status) | [1,1,1] | + | (jq)[.ocs.data, .ocs.data.files[]] \| map(.signers \| length) | [1,1,1] | + | (jq)[.ocs.data, .ocs.data.files[]] \| map([.signers[].identifyMethods[] \| select(.method == "account").value]) | [["signer2"],["signer2"],["signer2"]] | + When sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" + Then the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data.signers \| length | 1 | + | (jq)[.ocs.data.signers[].identifyMethods[] \| select(.method == "account").value] | ["signer2"] | + When sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" + Then the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data.signers \| length | 1 | + | (jq)[.ocs.data.signers[].identifyMethods[] \| select(.method == "account").value] | ["signer2"] | + + Scenario: Delete last envelope signer reverts both documents to draft without orphan requests + Given as user "admin" + And user "signer1" exists + And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/request-signature" + | files | [{"base64":"","name":"First.pdf"},{"base64":"","name":"Second.pdf"}] | + | signers | [{"identifyMethods":[{"method":"account","value":"signer1"}]}] | + | name | Last envelope signer | + | status | 0 | + And the response should have a status code 200 + And fetch field "(ENVELOPE_UUID)ocs.data.uuid" from previous JSON response + And sending "patch" to ocs "/apps/libresign/api/v1/request-signature" + | uuid | | + | signers | [{"identifyMethods":[{"method":"account","value":"signer1"}]}] | + | status | 1 | + And the response should have a status code 200 + And sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" + And the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data.nodeType | envelope | + | (jq).ocs.data.files \| length | 2 | + | (jq)[.ocs.data, .ocs.data.files[]] \| map(.status) | [1,1,1] | + | (jq)[.ocs.data, .ocs.data.files[]] \| map([.signers[].identifyMethods[] \| select(.method == "account").value]) | [["signer1"],["signer1"],["signer1"]] | + And fetch field "(ENVELOPE_ID)ocs.data.id" from previous JSON response + And fetch field '(SIGN_REQUEST_ID)(jq).ocs.data.signers[] | select(any(.identifyMethods[]; .method == "account" and .value == "signer1")) | .signRequestId' from previous JSON response + And fetch field "(FIRST_UUID)ocs.data.files.0.uuid" from previous JSON response + And fetch field "(SECOND_UUID)ocs.data.files.1.uuid" from previous JSON response + When sending "delete" to ocs "/apps/libresign/api/v1/sign/file_id//" + Then the response should have a status code 200 + When sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" + Then the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data.files \| length | 2 | + | (jq)[.ocs.data, .ocs.data.files[]] \| map(.signers) | [[],[],[]] | + | (jq)[.ocs.data, .ocs.data.files[]] \| map(.status) | [0,0,0] | + When sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" + Then the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data.signers | [] | + | (jq).ocs.data.status | 0 | + When sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" + Then the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data.signers | [] | + | (jq).ocs.data.status | 0 | From 5bd8a4ca4bd2c0d6f7cb655fdd714d2399e5a8c9 Mon Sep 17 00:00:00 2001 From: Carlo Marmo Date: Fri, 16 Oct 2026 19:27:35 +0400 Subject: [PATCH 2/2] fix: notify only once when removing an envelope signer Send the cancellation event for the envelope, not each document updated with it. Keep notifications for direct document removals. Check the email, notification and activity counts, and skip the child lookup when there are no identification methods. Signed-off-by: Carlo Marmo --- lib/Service/RequestSignatureService.php | 12 +-- .../integration/features/sign/cancel.feature | 75 +++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/lib/Service/RequestSignatureService.php b/lib/Service/RequestSignatureService.php index bac7d28546..8baafeb6da 100644 --- a/lib/Service/RequestSignatureService.php +++ b/lib/Service/RequestSignatureService.php @@ -612,13 +612,15 @@ public function validateSigners(array $data): void { } } - public function unassociateToUser(int $fileId, int $signRequestId): void { + public function unassociateToUser(int $fileId, int $signRequestId, bool $notify = true): void { $file = $this->fileMapper->getById($fileId); $signRequest = $this->signRequestMapper->getByFileIdAndSignRequestId($fileId, $signRequestId); $deletedOrder = $signRequest->getSigningOrder(); $groupedIdentifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($signRequestId); - $this->dispatchCancellationEventIfNeeded($signRequest, $file, $groupedIdentifyMethods); + if ($notify) { + $this->dispatchCancellationEventIfNeeded($signRequest, $file, $groupedIdentifyMethods); + } try { $this->signRequestMapper->delete($signRequest); @@ -655,12 +657,12 @@ private function propagateSignerDeletionToChildren(FileEntity $envelope, array $ return; } - $children = $this->fileMapper->getChildrenFiles($envelope->getId()); - if (empty($identifyMethods)) { return; } + $children = $this->fileMapper->getChildrenFiles($envelope->getId()); + foreach ($children as $child) { try { $this->identifyMethod->clearCache(); @@ -670,7 +672,7 @@ private function propagateSignerDeletionToChildren(FileEntity $envelope, array $ ); if ($childSignRequest->getId()) { - $this->unassociateToUser($child->getId(), $childSignRequest->getId()); + $this->unassociateToUser($child->getId(), $childSignRequest->getId(), notify: false); } } catch (\Throwable) { continue; diff --git a/tests/integration/features/sign/cancel.feature b/tests/integration/features/sign/cancel.feature index 639e930018..1aea021764 100644 --- a/tests/integration/features/sign/cancel.feature +++ b/tests/integration/features/sign/cancel.feature @@ -12,12 +12,14 @@ Feature: sign-request-cancel And sending "get" to ocs "/apps/libresign/api/v1/file/list?details=1" And fetch field "(FILE_ID)ocs.data.data.0.id" from previous JSON response And fetch field "(SIGN_REQUEST_ID)ocs.data.data.0.signers.0.signRequestId" from previous JSON response + And reset notifications of user "signer1" When sending "delete" to ocs "/apps/libresign/api/v1/sign/file_id//" Then the response should have a status code 200 When as user "signer1" And sending "get" to ocs "/apps/notifications/api/v2/notifications" Then the response should be a JSON array with the following mandatory values | key | value | + | (jq).ocs.data \| length | 1 | | (jq).ocs.data[0].subject | admin canceled the signature request for document | Scenario: Delete draft request does not send cancellation notification @@ -109,6 +111,11 @@ Feature: sign-request-cancel And fetch field '(SIGN_REQUEST_ID)(jq).ocs.data.signers[] | select(any(.identifyMethods[]; .method == "account" and .value == "signer1")) | .signRequestId' from previous JSON response And fetch field "(FIRST_UUID)ocs.data.files.0.uuid" from previous JSON response And fetch field "(SECOND_UUID)ocs.data.files.1.uuid" from previous JSON response + And reset notifications of user "signer1" + And reset activity of user "signer1" + And set the email of user "signer1" to "signer1@domain.test" + And activity preference "notify_email_libresign_sign_request_canceled" for user "signer1" is set to "1" + And my inbox is empty When sending "delete" to ocs "/apps/libresign/api/v1/sign/file_id//" Then the response should have a status code 200 When sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" @@ -131,6 +138,18 @@ Feature: sign-request-cancel | key | value | | (jq).ocs.data.signers \| length | 1 | | (jq)[.ocs.data.signers[].identifyMethods[] \| select(.method == "account").value] | ["signer2"] | + When as user "signer1" + And sending "get" to ocs "/apps/notifications/api/v2/notifications" + Then the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data \| length | 1 | + | (jq).ocs.data[0].subject | admin canceled the signature request for Envelope cancellation | + And there should be 1 emails in my inbox + When sending "get" to ocs "/apps/activity/api/v2/activity/libresign?since=0" + Then the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data \| length | 1 | Scenario: Delete last envelope signer reverts both documents to draft without orphan requests Given as user "admin" @@ -160,6 +179,11 @@ Feature: sign-request-cancel And fetch field '(SIGN_REQUEST_ID)(jq).ocs.data.signers[] | select(any(.identifyMethods[]; .method == "account" and .value == "signer1")) | .signRequestId' from previous JSON response And fetch field "(FIRST_UUID)ocs.data.files.0.uuid" from previous JSON response And fetch field "(SECOND_UUID)ocs.data.files.1.uuid" from previous JSON response + And reset notifications of user "signer1" + And reset activity of user "signer1" + And set the email of user "signer1" to "signer1@domain.test" + And activity preference "notify_email_libresign_sign_request_canceled" for user "signer1" is set to "1" + And my inbox is empty When sending "delete" to ocs "/apps/libresign/api/v1/sign/file_id//" Then the response should have a status code 200 When sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" @@ -181,3 +205,54 @@ Feature: sign-request-cancel | key | value | | (jq).ocs.data.signers | [] | | (jq).ocs.data.status | 0 | + When as user "signer1" + And sending "get" to ocs "/apps/notifications/api/v2/notifications" + Then the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data \| length | 1 | + | (jq).ocs.data[0].subject | admin canceled the signature request for Last envelope signer | + And there should be 1 emails in my inbox + When sending "get" to ocs "/apps/activity/api/v2/activity/libresign?since=0" + Then the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data \| length | 1 | + + Scenario: Directly deleting a child signer still sends its cancellation notification + Given as user "admin" + And user "signer1" exists + And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/request-signature" + | files | [{"base64":"","name":"First.pdf"},{"base64":"","name":"Second.pdf"}] | + | signers | [{"identifyMethods":[{"method":"account","value":"signer1"}]}] | + | name | Direct child cancellation | + | status | 0 | + And the response should have a status code 200 + And fetch field "(ENVELOPE_UUID)ocs.data.uuid" from previous JSON response + And sending "patch" to ocs "/apps/libresign/api/v1/request-signature" + | uuid | | + | signers | [{"identifyMethods":[{"method":"account","value":"signer1"}]}] | + | status | 1 | + And the response should have a status code 200 + And sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" + And the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq)[.ocs.data, .ocs.data.files[]] \| map(.status) | [1,1,1] | + And fetch field "(CHILD_ID)ocs.data.files.0.id" from previous JSON response + And fetch field '(SIGN_REQUEST_ID)(jq).ocs.data.files[0].signers[] | select(any(.identifyMethods[]; .method == "account" and .value == "signer1")) | .signRequestId' from previous JSON response + And reset notifications of user "signer1" + When sending "delete" to ocs "/apps/libresign/api/v1/sign/file_id//" + Then the response should have a status code 200 + When sending "get" to ocs "/apps/libresign/api/v1/file/validate/uuid/" + Then the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq)[.ocs.data, .ocs.data.files[]] \| map(.signers \| length) | [1,0,1] | + | (jq)[.ocs.data, .ocs.data.files[]] \| map(.status) | [1,0,1] | + When as user "signer1" + And sending "get" to ocs "/apps/notifications/api/v2/notifications" + Then the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data \| length | 1 | + | (jq).ocs.data[0].subject | admin canceled the signature request for First |