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 TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
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 { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAuthMethods } from '../../types/document-auth';
import { ZEnvelopeFieldSchema, ZFieldSchema } from '../../types/field';
import { ZRecipientLiteSchema } from '../../types/recipient';
import { isRecipientTurnBySigningOrder } from '../../utils/recipient-groups';
import { isRecipientExpired } from '../../utils/recipients';
import { isRecipientAuthorized } from '../document/is-recipient-authorized';
import { getTeamSettings } from '../team/get-team-settings';
@@ -194,9 +195,6 @@ export const getEnvelopeForRecipientSigning = async ({
},
},
},
orderBy: {
signingOrder: 'asc',
},
},
envelopeItems: true,
team: {
@@ -260,23 +258,9 @@ export const getEnvelopeForRecipientSigning = async ({
},
});
let isRecipientsTurn = true;
const currentRecipientIndex = envelope.recipients.findIndex((r) => r.token === token);
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 isRecipientsTurn =
envelope.documentMeta.signingOrder !== DocumentSigningOrder.SEQUENTIAL ||
isRecipientTurnBySigningOrder(envelope.recipients, recipient);
const sender = settings.includeSenderDetails
? {
@@ -1,5 +1,7 @@
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 = {
token: string;
@@ -17,11 +19,7 @@ export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOpt
},
include: {
documentMeta: true,
recipients: {
orderBy: {
signingOrder: 'asc',
},
},
recipients: true,
},
});
@@ -29,24 +27,11 @@ export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOpt
return true;
}
const { recipients } = envelope;
const currentRecipient = envelope.recipients.find((recipient) => recipient.token === token);
const currentRecipientIndex = recipients.findIndex((r) => r.token === token);
if (currentRecipientIndex === -1) {
if (!currentRecipient) {
return false;
}
for (let i = 0; i < currentRecipientIndex; i++) {
// 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;
return isRecipientTurnBySigningOrder(envelope.recipients, currentRecipient);
}
+7 -7
View File
@@ -126,7 +126,7 @@ export const mergeSteps = <T extends EditorRecipient>(
const targetStep = steps[targetStepIndex];
if (!sourceStep || !targetStep || sourceStepIndex === targetStepIndex) {
return recipients;
return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
}
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);
if (!targetStep || !mover || isCcRecipient(mover)) {
return recipients;
return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
}
if (targetStep.members.some((member) => member.formId === formId)) {
return recipients;
return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
}
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);
if (!mover || isCcRecipient(mover)) {
return recipients;
return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
}
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.
if (isSoloStep && (insertStepIndex === currentStepIndex || insertStepIndex === currentStepIndex + 1)) {
return recipients;
return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
}
const insertOrder =
@@ -234,7 +234,7 @@ export const reorderStep = <T extends EditorRecipient>(
const { steps, ccRecipients } = groupRecipientsBySigningOrder(recipients);
if (!steps[fromStepIndex] || fromStepIndex === toStepIndex) {
return recipients;
return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
}
const reorderedSteps = [...steps];
@@ -365,7 +365,7 @@ export const ungroupStep = <T extends EditorRecipient>(
const step = steps[stepIndex];
if (!step || step.members.length < 2) {
return recipients;
return normalizeGroupedSigningOrders(recipients, canUpdateRecipient);
}
const offsetByFormId = new Map(step.members.map((member, index) => [member.formId, index]));