feat(rejection): let the workflow continue after a rejection - #8706
Open
maia-andre wants to merge 5 commits into
Open
maia-andre wants to merge 5 commits into
maia-andre wants to merge 5 commits into
Conversation
…el it With `rejection_behavior = continue` a rejected signer no longer stops the others, but the file status was still derived from signatures alone: a signer who refused keeps `signed = null` forever, so the file stayed `PARTIAL_SIGNED` even after everybody had acted and the workflow had nothing left to wait for. Complete the file when no one is expected to act any more and at least one signature was actually applied, which is what LibreSign#8405 asks for: every signer is either `SIGNED` or `REJECTED`, and at least one of them signed. The file-level status becomes `SIGNED`, without a new `FileStatus`, because it describes the configured workflow having finished; the rejected signer keeps its own `REJECTED` status and is never converted. The behavior is read from the configuration frozen on the request, so a later policy change cannot complete or reopen an existing workflow, and `cancel` is unaffected: a rejection closes the file there and nothing is evaluated afterwards. When every signer rejects, no signature exists and the file is left as it was. LibreSign#8405 requires at least one signature to complete and forbids a new status for this case, so nothing else is decided here. Ref: LibreSign#8405 Assisted-by: Claude Code:claude-opus-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
A signer who refused keeps `signed = null`, and everything that decides who may still act read exactly that, so with `rejection_behavior = continue` the rejected signer was still treated as pending: the signing endpoint accepted their signature, the file list and the signer list still offered them the button, and in a sequential flow they kept the turn nobody would ever take. Refuse the signature itself, and stop counting a rejected signer as someone the workflow is waiting for: * `SignFileService::getSignRequestToSign()` rejects the attempt with its own message, next to the one for a document already signed; * `SignersLoader` no longer sets `canSign` for the viewer's own rejected entry; * `FileListService` leaves rejected signers out of the pending set, which also frees the sequential flow — `SequentialSigningService` already ignored them, so the summary now agrees with the gate that enforces it. With `cancel` none of this is reachable, because the rejection closes the file and `fileCanBeSigned()` refuses everyone. Ref: LibreSign#8405 Assisted-by: Claude Code:claude-opus-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
`reject.feature` only exercised rejections that close the workflow. Add the other half: with `rejection_behavior = continue`, one of two signers refuses and the rejection reports `workflowCanceled: false`, the signer who refused is no longer offered the signature and is refused if they ask for it anyway, the other signer signs, and the file reaches `SIGNED` while the signers keep their own results, `REJECTED` and `SIGNED`. The scenario installs the signing tools the way the TSA feature does, because a scenario that reaches a real signature needs them and the per-scenario reset clears their configuration. Ref: LibreSign#8405 Assisted-by: Claude Code:claude-opus-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
`settings.canSign` in the file response was set for the viewer's own entry whenever it was not signed, not rejected and not an observer. In a sequential flow that included a signer still waiting for the previous order, whose request stays `DRAFT` until `SequentialSigningService` activates it, so the response offered a signature the signing endpoint refuses with "You are not allowed to sign this document yet", and `MessagesLoader` told them "You need to sign this document". Apply the rules `SignerValidator` enforces when signing: the request must be `ABLE_TO_SIGN`, belong to a signing participant, and, in an ordered flow, no signer with a lower order may still be pending. Signed and rejected signers do not block the turn, which matches `SequentialSigningService::hasPendingLowerOrderSigners()` and lets the workflow continue after a rejection. The check uses the signers already loaded for the response, so it adds no query. The approver of an identification document is unaffected: their `canSign` comes from `SettingsLoader`, and this code only ever sets the flag. The test for the rejected signer becomes a matrix that also covers the parallel, draft, observer and sequential cases. Ref: LibreSign#8405 Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
…inues In a sequential flow, a rejection that does not cancel the workflow ended the signer's turn without activating the next order: the signers after them stayed `DRAFT` forever, and the document could never reach `SIGNED`. `SignatureRejectionService` now calls `SequentialSigningService::releaseNextOrder()` inside the rejection transaction for every document the signer rejected, so an envelope releases the next order on each of its documents. A failure while releasing rolls the rejection back. `isOrderFullyCompleted()` also treats `REJECTED` as done, matching `hasPendingLowerOrderSigners()`; otherwise the release would still wait for the rejected signer. Ref: LibreSign#8405 Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #8405. This is the second of the three pull requests agreed in the issue: #8423 brought the policy model, this one brings the behavior, and the third will adapt #8398 to
rejection_visibilityandsettings.canSign.📝 Summary
With
rejection_behavior = continue, a rejection no longer stopped the other signers. Nothing after that point knew about rejected signers, though. The file could never finish, the signer who refused was still offered the signature, and in a sequential flow the signers after them waited forever. This PR covers the items that step 2 of the issue lists:continuecompletes the file. When every signer isSIGNEDorREJECTEDand at least one of them signed, the file becomesSIGNED. No newFileStatusis added, and the rejected signer keeps its ownREJECTEDstatus. The behavior comes from the configuration frozen on the request, so a later policy change cannot complete an existing workflow or reopen it. Observers were already left out of that evaluation.SignFileServicerefuses the attempt with its own message.FileListServicestops counting rejected signers as pending, and the file list stops offering them the button.settings.canSignisfalsefor rejected signers, for observers, and for a signer whose turn has not come.SignersLoadernow applies the rules thatSignerValidatorenforces at signing time. The request must beABLE_TO_SIGNand belong to a signing participant. In an ordered flow, no signer with a lower order may still be pending. Before this change, aDRAFTsigner waiting for their turn was offered a signature that the endpoint then refused.SignatureRejectionServicecallsSequentialSigningService::releaseNextOrder()inside the rejection transaction, for every document the signer rejected, so an envelope releases the next order on each of its documents.isOrderFullyCompleted()treatsREJECTEDas done, the same wayhasPendingLowerOrderSigners()already did. If the release fails, the rejection rolls back.cancelis unchanged: the rejection still closes the file, and none of the code above runs.Composition and reading order
+582/−11 in 11 files. About 117 of those lines are production code and 465 are tests (unit tests and one Behat scenario). There is no generated code: the response shapes do not change, and
composer openapiproduces no diff.The commits can be read one by one:
feat(rejection): complete the workflow when a rejection does not cancel it:SignFileService(start here)fix(rejection): keep a rejected signer out of the signing flow:SignFileService,SignersLoader,FileListServicetest(rejection): cover the continue behavior end to end: a new scenario inreject.featurefix(sign): offer the signature only when it is the signer's turn:SignersLoaderfix(rejection): release the next signing order when the workflow continues:SignatureRejectionService,SequentialSigningService❓ One point for you
When every signer rejects, no signature exists, and the file is left as it was. The issue requires at least one signature before a file can complete, and it rules out a new status for this case, so this PR does not decide it. Should the file stay as it is, should it be canceled like with
cancel, or should this be tracked separately?🧪 How to test
The Behat scenario in
tests/integration/features/sign/reject.featurecovers the parallel case end to end:cd tests/integration vendor/bin/behat features/sign/reject.feature -vTwo signers, with
rejection_enabled = trueandrejection_behavior = continue. The first signer rejects, and the response carriesworkflowCanceled: false. That signer is no longer offered the signature, and a direct signing request from them is refused. The second signer signs, and the file reachesSIGNEDwhile the signers keepREJECTEDandSIGNED.Unit tests cover the sequential flow: turn handling in
canSign, releasing the next order after a rejection, envelopes, and rollback. To check it by hand:continue(therejection_enabledandrejection_behaviorpolicies).settings.canSignbecomestruefor them.settings.canSign = falseuntil signer 2 signs. When signer 3 signs, the file becomesSIGNED.⚙️ API / Back‑end changes
continue, the file becomesSIGNEDonce every signer has signed or rejected and at least one signature existssettings.canSign:falsefor rejected signers, observers, and signers whose turn has not comecomposer openapiif necessary: ran it, no diff✅ Checklist
SignFileServiceTest,FileListServiceTest,SignersLoaderTest,SignatureRejection/,SequentialSigningServiceTest🤖 AI (if applicable)