Skip to content

fix: freeze the signature flow once the request is sent - #9062

Open
paulo-raoni wants to merge 4 commits into
LibreSign:mainfrom
paulo-raoni:fix/8715-signature-flow-freeze
Open

paulo-raoni wants to merge 4 commits into
LibreSign:mainfrom
paulo-raoni:fix/8715-signature-flow-freeze

Conversation

@paulo-raoni

Copy link
Copy Markdown
Contributor

Refs: #8715

📝 Summary

Next slice of #8715: signature_flow follows the request-snapshot freeze. This is option 1 from the issue discussion: strict backend freeze, the request sidebar updated in the same PR, and the broad catch in saveFile() narrowed.

One commit per step:

  • fix: stop creating a second file when updating by node id fails – in RequestSignatureService::saveFile() only DoesNotExistException from getByNodeId() leads to the new-file path. Policy and status failures now reach the caller instead of being swallowed into a second File row for the same node.
  • fix: freeze the signature flow once the request is sent – SignatureFlowFilePolicyApplier::sync() returns before any resolution when isPolicySnapshotFrozen() is true. The submitted value is compared with the stored column, strictly: none is not parallel.
  • feat: tell authenticated clients whether the request policy snapshot is frozen – new boolean policySnapshotFrozen on LibresignFileSummary and LibresignDetailedFile (file list and request-signature / file-save responses). The status alone cannot tell, because a request that returned to DRAFT stays frozen. LibresignValidatedFile and the validation metadata are unchanged, so the file/validate endpoints do not carry it and the marker itself is still not exposed there. The sign page payloads (...ForSignRequest, including the envelopeFiles initial state) do not carry it either; FileListServiceTest covers that.
  • fix: keep the stored signature flow in the request sidebar once frozen – once frozen, the sidebar shows and sends the stored flow (none included), hides "Sign in order" and "Use this as my default signing order" the same way it does when the policy forbids the override, and no longer rewrites the file flow from a policy forced later.
Request Submitted signature_flow Result
Frozen (marker, or status ABLE_TO_SIGN or later without marker, or document of a frozen envelope) nothing stored column and snapshot kept, no resolution, no write
Frozen the stored value, none included no-op
Frozen any other value, parallel over a stored none included 422, "The signing order cannot be changed after the signing flow has started."
Frozen, returned to DRAFT same three rows same results
Draft never sent anything re-resolved on save, as before
New file anything unchanged

Signers added later use the stored flow: every consumer reads the column, which no longer moves.

Behaviour change for API clients: a client that PATCHes a different signature_flow after the request was sent now gets 422. A request created through the API with the default status is ABLE_TO_SIGN, so it is frozen at creation. On the node id path, any failure other than "node not registered yet" is now returned instead of creating another file.

Not in this PR: add_footer, docmdp, the other snapshot keys, and the envelope rule for files added after the freeze. An envelope entity only receives its flow through a PATCH by uuid; that is unchanged here and belongs with the envelope rule.

🧪 How to test

composer test:unit -- --filter 'SignatureFlowFilePolicyApplierTest|RequestSignatureServiceTest|FileStatusServiceTest|FileListServiceTest'
vendor/bin/phpunit -c tests/php/phpunit.xml tests/php/Api/Controller/RequestSignatureControllerTest.php
npx vitest run src/tests/components/RightSidebar/RequestSignatureTab.spec.ts src/tests/views/Settings/PolicyWorkbench/settings/signature-flow/model.spec.ts
  • SignatureFlowFilePolicyApplierTest: a frozen request keeps its flow with nothing submitted, with the stored flow resent, after returning to DRAFT and without a marker; a different flow gets 422 (parallel over none included); a signer added after the freeze is ordered by the stored flow.
  • RequestSignatureServiceTest: a refused update of a registered node does not create another file; an unregistered node is still created.
  • FileStatusServiceTest, FileListServiceTest: the flag on the three payload paths, for a draft, a sent request back in DRAFT and a request sent before the marker existed.
  • RequestSignatureControllerTest: PATCH with another flow on a sent request returns 422 with the message above.
  • Vitest: a frozen request sends the stored flow and hides the switches, a later forced policy does not rewrite it, a draft behaves as before.

Manually: send a request with two signers and reopen it in the sidebar: the signing order switch is gone and adding a signer saves without error. PATCH /api/v1/request-signature with another policy.overrides.signature_flow on that uuid returns 422.

Validation evidence

  • Focused regression/acceptance test – red before the change, green after
  • Relevant lint/type/static-analysis checks – composer psalm, php-cs-fixer dry-run on the touched files, npm run lint, npm run ts:check
  • Broader suite required by the changed surface – partly, see below
  • CI reviewed

Ran locally: tests/php/Unit/Service/Policy (952 tests); --filter 'RequestSignatureService|FileStatusService|Envelope|SequentialSigning|FileListService' (324 tests); tests/php/Api/Controller/RequestSignatureControllerTest.php and FileControllerTest.php; Behat features/sign/sequential_signing.feature (2 scenarios); the focused Vitest files; npm test once.

Not run locally: the whole PHPUnit unit and api suites, the other Behat features, and Playwright (signature-flow-policy-request-sidebar.spec.ts only toggles the switch on a draft, which is unchanged; it was read, not run).

npm test has one failure, in src/tests/views/Validation.spec.ts ("updates the policies store when requester effective policies are returned"). It is not caused by this PR: the spec expects policiesStore.setPolicies to be called, and Validation.vue stopped calling it in 37d92dd. This PR does not touch that view, its spec or the policies store.

tests/php/Integration/Controller/PageControllerTest.php has 11 errors (ArgumentCountError: the test builds PageController without $validationEffectivePolicyService). They are the same with every lib/ file of this branch restored to main, so they are not caused by this PR either.

🎨 UI / Front-end changes

  • On a request that already entered the signing flow, the request sidebar no longer shows "Sign in order" and "Use this as my default signing order", and it saves with the stored flow.

  • Screenshots before/after added for user-visible changes – not yet

  • Tested in relevant browsers when applicable – not yet

  • Component/unit (Vitest) and/or E2E (Playwright) tests added or updated – Vitest

  • Accessibility verified when applicable – no new control; two existing switches are hidden

  • Design review approved when applicable

  • Documentation updated when applicable

⚙️ API / Back-end changes

  • signature_flow is frozen with the request policy snapshot; a different value after the freeze returns 422.
  • New required boolean policySnapshotFrozen on LibresignFileSummary and LibresignDetailedFile.
  • saveFile() by node id returns policy failures instead of creating another file.
  • Unit and/or integration tests added or updated
  • Authorization and negative paths checked when applicable – 422 at the applier and through the API; a user who may not override can still resend the stored flow
  • Capabilities updated when applicable – not applicable
  • Documentation updated when applicable – not yet
  • API documentation regenerated with composer openapi when necessary – and npm run typescript:generate; the generated diff is only the new field

✅ Checklist

  • I have read and followed the contribution guide.
  • The diff does not contain unrelated refactors or generated changes.
  • Commits follow Conventional Commits where applicable.
  • All commits include the required DCO sign-off.

🤖 AI (if applicable)

  • The content of this PR was partially or fully generated using AI

Only a node that is not registered yet falls through to the new-file path; policy failures now reach the caller.

Signed-off-by: Paulo Raoni <raoni_jkd@hotmail.com>
A frozen request keeps its stored flow: the same value is accepted, another one is refused with 422.

Signed-off-by: Paulo Raoni <raoni_jkd@hotmail.com>
…is frozen

The file list and request-signature responses carry policySnapshotFrozen; validation responses are unchanged.

Signed-off-by: Paulo Raoni <raoni_jkd@hotmail.com>
The sidebar sends the stored flow, none included, and no longer offers the signing order switch.

Signed-off-by: Paulo Raoni <raoni_jkd@hotmail.com>
@paulo-raoni
paulo-raoni requested a review from a team as a code owner October 9, 2026 00:23

@YvesCesar YvesCesar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @paulo-raoni !

I have a few revisions. Can you take a look?

];
// The sign page formats files without a user and may be public.
if ($user !== null) {
$file['policySnapshotFrozen'] = FileStatusService::isPolicySnapshotFrozen($fileEntity);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

policySnapshotFrozen here only looks at the file itself, but the backend also treats a document as frozen when its envelope is frozen (AbstractFilePolicyApplier::isPolicySnapshotFrozen checks $envelope too).

So a child document that is still in DRAFT inside a frozen envelope comes back with false. The sidebar then takes the non-frozen path, where toRequestSignatureFlowOverride turns none into parallel, and the save fails with 422.

Could you make the flag use the same rule as the backend (file or parent envelope frozen)? Lines 392 and 839 need the same change. A shared method that takes the envelope into account would keep both sides from drifting apart.

if (!is_null($nodeId)) {
try {
$file = $this->fileMapper->getByNodeId($nodeId);
} catch (DoesNotExistException) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: node_id has no unique index, and the old catch (\Throwable) could create a second row for the same node when policy sync failed. On installs that already have such duplicates, getByNodeId now throws MultipleObjectsReturnedException and the save returns 500. It may be worth catching it and returning a clear error, or handling it in a follow-up.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants