feat: use group-aware turn checks for sequential signing

This commit is contained in:
David Nguyen
2026-08-04 17:28:32 +10:00
parent d52000f648
commit 9c2f28a4f4
3 changed files with 19 additions and 50 deletions
@@ -5,13 +5,14 @@ import EnvelopeSchema from '@documenso/prisma/generated/zod/modelSchema/Envelope
import SignatureSchema from '@documenso/prisma/generated/zod/modelSchema/SignatureSchema'; import SignatureSchema from '@documenso/prisma/generated/zod/modelSchema/SignatureSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema'; import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
import { DocumentSigningOrder, DocumentStatus, EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client'; import { DocumentSigningOrder, DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
import { z } from 'zod'; import { z } from 'zod';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAuthMethods } from '../../types/document-auth'; import type { TDocumentAuthMethods } from '../../types/document-auth';
import { ZEnvelopeFieldSchema, ZFieldSchema } from '../../types/field'; import { ZEnvelopeFieldSchema, ZFieldSchema } from '../../types/field';
import { ZRecipientLiteSchema } from '../../types/recipient'; import { ZRecipientLiteSchema } from '../../types/recipient';
import { isRecipientTurnBySigningOrder } from '../../utils/recipient-groups';
import { isRecipientExpired } from '../../utils/recipients'; import { isRecipientExpired } from '../../utils/recipients';
import { isRecipientAuthorized } from '../document/is-recipient-authorized'; import { isRecipientAuthorized } from '../document/is-recipient-authorized';
import { getTeamSettings } from '../team/get-team-settings'; import { getTeamSettings } from '../team/get-team-settings';
@@ -194,9 +195,6 @@ export const getEnvelopeForRecipientSigning = async ({
}, },
}, },
}, },
orderBy: {
signingOrder: 'asc',
},
}, },
envelopeItems: true, envelopeItems: true,
team: { team: {
@@ -260,23 +258,9 @@ export const getEnvelopeForRecipientSigning = async ({
}, },
}); });
let isRecipientsTurn = true; const isRecipientsTurn =
envelope.documentMeta.signingOrder !== DocumentSigningOrder.SEQUENTIAL ||
const currentRecipientIndex = envelope.recipients.findIndex((r) => r.token === token); isRecipientTurnBySigningOrder(envelope.recipients, recipient);
if (envelope.documentMeta.signingOrder === DocumentSigningOrder.SEQUENTIAL && currentRecipientIndex !== -1) {
for (let i = 0; i < currentRecipientIndex; i++) {
// CC recipients have no action to take, so they can never block the flow.
if (envelope.recipients[i].role === RecipientRole.CC) {
continue;
}
if (envelope.recipients[i].signingStatus !== SigningStatus.SIGNED) {
isRecipientsTurn = false;
break;
}
}
}
const sender = settings.includeSenderDetails const sender = settings.includeSenderDetails
? { ? {
@@ -1,5 +1,7 @@
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client'; import { DocumentSigningOrder, EnvelopeType } from '@prisma/client';
import { isRecipientTurnBySigningOrder } from '../../utils/recipient-groups';
export type GetIsRecipientTurnOptions = { export type GetIsRecipientTurnOptions = {
token: string; token: string;
@@ -17,11 +19,7 @@ export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOpt
}, },
include: { include: {
documentMeta: true, documentMeta: true,
recipients: { recipients: true,
orderBy: {
signingOrder: 'asc',
},
},
}, },
}); });
@@ -29,24 +27,11 @@ export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOpt
return true; return true;
} }
const { recipients } = envelope; const currentRecipient = envelope.recipients.find((recipient) => recipient.token === token);
const currentRecipientIndex = recipients.findIndex((r) => r.token === token); if (!currentRecipient) {
if (currentRecipientIndex === -1) {
return false; return false;
} }
for (let i = 0; i < currentRecipientIndex; i++) { return isRecipientTurnBySigningOrder(envelope.recipients, currentRecipient);
// CC recipients have no action to take, so they can never block the flow.
if (recipients[i].role === RecipientRole.CC) {
continue;
}
if (recipients[i].signingStatus !== SigningStatus.SIGNED) {
return false;
}
}
return true;
} }
+7 -7
View File
@@ -126,7 +126,7 @@ export const mergeSteps = <T extends EditorRecipient>(
const targetStep = steps[targetStepIndex]; const targetStep = steps[targetStepIndex];
if (!sourceStep || !targetStep || sourceStepIndex === targetStepIndex) { if (!sourceStep || !targetStep || sourceStepIndex === targetStepIndex) {
return recipients; return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
} }
const sourceFormIds = new Set(sourceStep.members.map((member) => member.formId)); const sourceFormIds = new Set(sourceStep.members.map((member) => member.formId));
@@ -162,11 +162,11 @@ export const moveRecipientToStep = <T extends EditorRecipient>(
const mover = recipients.find((recipient) => recipient.formId === formId); const mover = recipients.find((recipient) => recipient.formId === formId);
if (!targetStep || !mover || isCcRecipient(mover)) { if (!targetStep || !mover || isCcRecipient(mover)) {
return recipients; return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
} }
if (targetStep.members.some((member) => member.formId === formId)) { if (targetStep.members.some((member) => member.formId === formId)) {
return recipients; return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
} }
const remaining = recipients.filter((recipient) => recipient.formId !== formId); const remaining = recipients.filter((recipient) => recipient.formId !== formId);
@@ -197,7 +197,7 @@ export const extractRecipientToNewStep = <T extends EditorRecipient>(
const mover = recipients.find((recipient) => recipient.formId === formId); const mover = recipients.find((recipient) => recipient.formId === formId);
if (!mover || isCcRecipient(mover)) { if (!mover || isCcRecipient(mover)) {
return recipients; return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
} }
const currentStepIndex = steps.findIndex((step) => step.members.some((member) => member.formId === formId)); const currentStepIndex = steps.findIndex((step) => step.members.some((member) => member.formId === formId));
@@ -205,7 +205,7 @@ export const extractRecipientToNewStep = <T extends EditorRecipient>(
// Dropping a solo step into the gap directly above or below itself is a no-op. // Dropping a solo step into the gap directly above or below itself is a no-op.
if (isSoloStep && (insertStepIndex === currentStepIndex || insertStepIndex === currentStepIndex + 1)) { if (isSoloStep && (insertStepIndex === currentStepIndex || insertStepIndex === currentStepIndex + 1)) {
return recipients; return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
} }
const insertOrder = const insertOrder =
@@ -234,7 +234,7 @@ export const reorderStep = <T extends EditorRecipient>(
const { steps, ccRecipients } = groupRecipientsBySigningOrder(recipients); const { steps, ccRecipients } = groupRecipientsBySigningOrder(recipients);
if (!steps[fromStepIndex] || fromStepIndex === toStepIndex) { if (!steps[fromStepIndex] || fromStepIndex === toStepIndex) {
return recipients; return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
} }
const reorderedSteps = [...steps]; const reorderedSteps = [...steps];
@@ -365,7 +365,7 @@ export const ungroupStep = <T extends EditorRecipient>(
const step = steps[stepIndex]; const step = steps[stepIndex];
if (!step || step.members.length < 2) { if (!step || step.members.length < 2) {
return recipients; return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
} }
const offsetByFormId = new Map(step.members.map((member, index) => [member.formId, index])); const offsetByFormId = new Map(step.members.map((member, index) => [member.formId, index]));