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