From 2140d132d706e70aee00e8acccd170954d4c474b Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 11 Sep 2026 14:28:15 +0300 Subject: [PATCH] feat: persist and enforce the allow document rejection setting Accepts allowDocumentRejection in the organisation and team settings update routes, adds it to the default organisation (true) and team (inherit) settings, and copies the merged value into the document meta when a document or template is created via extractDerivedDocumentMeta. Documents created from a template take the template's value, with an optional override on envelope.use. Exposes the field in the recipient signing response and refuses rejectDocumentWithToken requests for documents that disallow rejection, since the endpoint can be called directly without the UI. Extends the document preferences and envelope settings e2e tests to cover inheritance into the team settings and new documents, and persistence of the per-document value from the editor. --- .../envelope-editor-v2/envelope-settings.spec.ts | 9 +++++++++ .../organisation-team-preferences.spec.ts | 10 ++++++++++ .../document/reject-document-with-token.ts | 14 +++++++++++++- .../envelope/get-envelope-for-recipient-signing.ts | 1 + .../template/create-document-from-template.ts | 2 ++ packages/lib/utils/document.ts | 3 +++ packages/lib/utils/organisations.ts | 1 + packages/lib/utils/teams.ts | 1 + .../server/envelope-router/use-envelope.types.ts | 2 ++ .../update-organisation-settings.ts | 2 ++ .../update-organisation-settings.types.ts | 1 + .../server/team-router/update-team-settings.ts | 2 ++ .../team-router/update-team-settings.types.ts | 1 + 13 files changed, 48 insertions(+), 1 deletion(-) diff --git a/packages/app-tests/e2e/envelope-editor-v2/envelope-settings.spec.ts b/packages/app-tests/e2e/envelope-editor-v2/envelope-settings.spec.ts index 27e038524..4a8f8fedf 100644 --- a/packages/app-tests/e2e/envelope-editor-v2/envelope-settings.spec.ts +++ b/packages/app-tests/e2e/envelope-editor-v2/envelope-settings.spec.ts @@ -24,6 +24,7 @@ const TEST_SETTINGS_VALUES = { subject: 'E2E settings subject', message: 'E2E settings message', language: 'French', + allowDocumentRejection: 'No', dateFormat: 'DD/MM/YYYY', timezone: 'Europe/London', distributionMethod: 'None', @@ -104,6 +105,10 @@ const runSettingsFlow = async ({ root }: TEnvelopeEditorSurface, { externalId, i await root.getByRole('option', { name: 'Upload' }).click(); await clickSettingsDialogHeader(root); + await getComboboxByLabel(root, 'Allow Document Rejection').click(); + await root.getByRole('option', { name: TEST_SETTINGS_VALUES.allowDocumentRejection, exact: true }).click(); + await clickSettingsDialogHeader(root); + await getComboboxByLabel(root, 'Date Format').click(); await root.getByRole('option', { name: TEST_SETTINGS_VALUES.dateFormat, exact: true }).click(); await clickSettingsDialogHeader(root); @@ -265,6 +270,9 @@ const runSettingsFlow = async ({ root }: TEnvelopeEditorSurface, { externalId, i await expect(root.locator('input[name="meta.redirectUrl"]')).toHaveValue(TEST_SETTINGS_VALUES.redirectUrl); await expect(getComboboxByLabel(root, 'Language')).toContainText(TEST_SETTINGS_VALUES.language); await expect(getComboboxByLabel(root, 'Allowed Signature Types')).not.toContainText('Upload'); + await expect(getComboboxByLabel(root, 'Allow Document Rejection')).toContainText( + TEST_SETTINGS_VALUES.allowDocumentRejection, + ); await expect(getComboboxByLabel(root, 'Date Format')).toContainText(TEST_SETTINGS_VALUES.dateFormat); await expect(getComboboxByLabel(root, 'Time Zone')).toContainText(TEST_SETTINGS_VALUES.timezone); await expect(root.locator('[data-testid="documentDistributionMethodSelectValue"]')).toContainText( @@ -384,6 +392,7 @@ const assertEnvelopeSettingsPersistedInDatabase = async ({ expect(envelope.documentMeta.drawSignatureEnabled).toBe(true); expect(envelope.documentMeta.typedSignatureEnabled).toBe(true); expect(envelope.documentMeta.uploadSignatureEnabled).toBe(false); + expect(envelope.documentMeta.allowDocumentRejection).toBe(false); expect(envelope.documentMeta.emailSettings).toMatchObject(DB_EXPECTED_VALUES.emailSettings); const authOptions = parseAuthOptions(envelope.authOptions); diff --git a/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts b/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts index 895b0c5aa..0ae6a1bbe 100644 --- a/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts +++ b/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts @@ -37,6 +37,10 @@ test('[ORGANISATIONS]: manage document preferences', async ({ page }) => { await page.getByRole('option', { name: 'Upload' }).click(); await page.keyboard.press('Escape'); + // Disable document rejection + await page.getByTestId('allow-document-rejection-trigger').click(); + await page.getByRole('option', { name: 'No', exact: true }).click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your document preferences have been updated').first()).toBeVisible(); @@ -68,10 +72,14 @@ test('[ORGANISATIONS]: manage document preferences', async ({ page }) => { expect(teamSettings.typedSignatureEnabled).toEqual(true); expect(teamSettings.uploadSignatureEnabled).toEqual(false); expect(teamSettings.drawSignatureEnabled).toEqual(false); + expect(teamSettings.allowDocumentRejection).toEqual(false); // Edit the team settings await page.goto(`/t/${team.url}/settings/document`); + // Document rejection is left untouched so it keeps inheriting from the organisation. + await expect(page.getByTestId('allow-document-rejection-status')).toHaveText('Inherited'); + await page.getByTestId('document-visibility-trigger').click(); await page.getByRole('option', { name: 'Everyone can access and view' }).click(); await page.getByTestId('document-language-trigger').click(); @@ -102,6 +110,7 @@ test('[ORGANISATIONS]: manage document preferences', async ({ page }) => { expect(updatedTeamSettings.typedSignatureEnabled).toEqual(true); expect(updatedTeamSettings.uploadSignatureEnabled).toEqual(false); expect(updatedTeamSettings.drawSignatureEnabled).toEqual(false); + expect(updatedTeamSettings.allowDocumentRejection).toEqual(false); const document = await seedTeamDocumentWithMeta(team); @@ -120,6 +129,7 @@ test('[ORGANISATIONS]: manage document preferences', async ({ page }) => { expect(documentMeta.language).toEqual('pl'); expect(documentMeta.timezone).toEqual('Europe/London'); expect(documentMeta.dateFormat).toEqual('MM/dd/yyyy'); + expect(documentMeta.allowDocumentRejection).toEqual(false); }); test('[ORGANISATIONS]: manage branding preferences', async ({ page }) => { diff --git a/packages/lib/server-only/document/reject-document-with-token.ts b/packages/lib/server-only/document/reject-document-with-token.ts index a8555b42b..52d46f942 100644 --- a/packages/lib/server-only/document/reject-document-with-token.ts +++ b/packages/lib/server-only/document/reject-document-with-token.ts @@ -33,7 +33,11 @@ export async function rejectDocumentWithToken({ token, id, reason, requestMetada envelope: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT), }, include: { - envelope: true, + envelope: { + include: { + documentMeta: true, + }, + }, }, }); @@ -51,6 +55,14 @@ export async function rejectDocumentWithToken({ token, id, reason, requestMetada }); } + // Hiding the reject button on the signing page is not enough because the + // recipient can call this endpoint directly, so enforce the setting here. + if (!envelope.documentMeta.allowDocumentRejection) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: `Document ${envelope.id} does not allow rejection`, + }); + } + assertRecipientNotExpired(recipient); // Update the recipient status to rejected diff --git a/packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts b/packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts index 116238f3b..d4623c932 100644 --- a/packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts +++ b/packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts @@ -48,6 +48,7 @@ export const ZEnvelopeForSigningResponse = z.object({ uploadSignatureEnabled: true, drawSignatureEnabled: true, allowDictateNextSigner: true, + allowDocumentRejection: true, language: true, }), recipients: ZRecipientLiteSchema.pick({ diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index 47158c3f0..74f0cdf6c 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -113,6 +113,7 @@ export type CreateDocumentFromTemplateOptions = { uploadSignatureEnabled?: boolean; drawSignatureEnabled?: boolean; envelopeExpirationPeriod?: TEnvelopeExpirationPeriod | null; + allowDocumentRejection?: boolean; }; formValues?: TDocumentFormValues; @@ -542,6 +543,7 @@ export const createDocumentFromTemplate = async ({ drawSignatureEnabled: override?.drawSignatureEnabled ?? template.documentMeta?.drawSignatureEnabled, allowDictateNextSigner: override?.allowDictateNextSigner ?? template.documentMeta?.allowDictateNextSigner, envelopeExpirationPeriod: override?.envelopeExpirationPeriod ?? template.documentMeta?.envelopeExpirationPeriod, + allowDocumentRejection: override?.allowDocumentRejection ?? template.documentMeta?.allowDocumentRejection, }, signatureLevel, ), diff --git a/packages/lib/utils/document.ts b/packages/lib/utils/document.ts index c64a09f67..e18114a26 100644 --- a/packages/lib/utils/document.ts +++ b/packages/lib/utils/document.ts @@ -70,6 +70,9 @@ export const extractDerivedDocumentMeta = ( // Reminder settings. reminderSettings: meta.reminderSettings ?? settings.reminderSettings ?? null, + + // Rejection settings. + allowDocumentRejection: meta.allowDocumentRejection ?? settings.allowDocumentRejection, } satisfies Omit; }; diff --git a/packages/lib/utils/organisations.ts b/packages/lib/utils/organisations.ts index b643ba6aa..e7736a218 100644 --- a/packages/lib/utils/organisations.ts +++ b/packages/lib/utils/organisations.ts @@ -111,6 +111,7 @@ export const generateDefaultOrganisationSettings = (): Omit