From 9c2f28a4f42efe76f80f98489ec458942f9bbab6 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 4 Aug 2026 17:28:32 +1000 Subject: [PATCH] feat: use group-aware turn checks for sequential signing --- .../get-envelope-for-recipient-signing.ts | 26 ++++------------- .../recipient/get-is-recipient-turn.ts | 29 +++++-------------- packages/lib/utils/recipient-groups.ts | 14 ++++----- 3 files changed, 19 insertions(+), 50 deletions(-) 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..652a51925 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 @@ -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 ? { diff --git a/packages/lib/server-only/recipient/get-is-recipient-turn.ts b/packages/lib/server-only/recipient/get-is-recipient-turn.ts index fae6e130b..cbbd8020c 100644 --- a/packages/lib/server-only/recipient/get-is-recipient-turn.ts +++ b/packages/lib/server-only/recipient/get-is-recipient-turn.ts @@ -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); } diff --git a/packages/lib/utils/recipient-groups.ts b/packages/lib/utils/recipient-groups.ts index 16d45a4dd..efac652f5 100644 --- a/packages/lib/utils/recipient-groups.ts +++ b/packages/lib/utils/recipient-groups.ts @@ -126,7 +126,7 @@ export const mergeSteps = ( 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 = ( 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 = ( 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 = ( // 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 = ( 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 = ( 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]));