This commit is contained in:
David Nguyen
2026-08-26 12:10:29 +10:00
parent 9dc83bdb06
commit 6db71b13d4
70 changed files with 5964 additions and 892 deletions
@@ -12,6 +12,7 @@ import type { EnvelopeIdOptions } from '../../utils/envelope';
import { mapRecipientToLegacyRecipient } from '../../utils/recipients';
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { assertCompatibleRecipientGrouping } from '../signature-level/assert-compatible-recipient-grouping';
import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role';
export interface CreateEnvelopeRecipientsOptions {
@@ -91,6 +92,13 @@ export const createEnvelopeRecipients = async ({
});
}
// Grouping is a property of the whole recipient set, so check the state the
// envelope will be left in rather than the incoming batch alone.
assertCompatibleRecipientGrouping({
signatureLevel: envelope.signatureLevel,
recipients: [...envelope.recipients, ...recipientsToCreate],
});
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
...recipient,
email: recipient.email.toLowerCase(),
@@ -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);
}
@@ -1,7 +1,8 @@
import { prisma } from '@documenso/prisma';
import { EnvelopeType, RecipientRole } from '@prisma/client';
import { EnvelopeType } from '@prisma/client';
import { mapDocumentIdToSecondaryId } from '../../utils/envelope';
import { getNextDictatableRecipient } from '../../utils/recipient-groups';
export const getNextPendingRecipient = async ({
documentId,
@@ -16,33 +17,17 @@ export const getNextPendingRecipient = async ({
type: EnvelopeType.DOCUMENT,
secondaryId: mapDocumentIdToSecondaryId(documentId),
},
// CC recipients are informational only and never take part in signing,
// so they must never be offered as the next pending recipient.
role: {
not: RecipientRole.CC,
},
},
orderBy: [
{
signingOrder: {
sort: 'asc',
nulls: 'last',
},
},
{
id: 'asc',
},
],
});
const currentIndex = recipients.findIndex((r) => r.id === currentRecipientId);
const nextRecipient = getNextDictatableRecipient({ recipients, currentRecipientId });
if (currentIndex === -1 || currentIndex === recipients.length - 1) {
if (!nextRecipient) {
return null;
}
return {
...recipients[currentIndex + 1],
...nextRecipient,
token: '',
};
};
@@ -2,6 +2,7 @@ import { prisma } from '@documenso/prisma';
import { FieldType } from '@prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { getAssistableRecipientsWhereInput } from '../../utils/recipients';
export interface GetRecipientsForAssistantOptions {
token: string;
@@ -23,9 +24,9 @@ export const getRecipientsForAssistant = async ({ token }: GetRecipientsForAssis
let recipients = await prisma.recipient.findMany({
where: {
envelopeId: assistant.envelopeId,
signingOrder: {
gte: assistant.signingOrder ?? 0,
},
// The assistant themself plus strictly later steps — never their own
// group peers, with null orders treated as the tail step.
AND: [getAssistableRecipientsWhereInput(assistant)],
},
include: {
fields: {
@@ -17,6 +17,7 @@ import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { assertCompatibleRecipientGrouping } from '../signature-level/assert-compatible-recipient-grouping';
import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role';
export interface SetDocumentRecipientsOptions {
@@ -98,6 +99,13 @@ export const setDocumentRecipients = async ({
});
}
// This route replaces the whole recipient set, so the payload is the
// resulting state.
assertCompatibleRecipientGrouping({
signatureLevel: envelope.signatureLevel,
recipients,
});
const normalizedRecipients = recipients.map((recipient) => ({
...recipient,
email: recipient.email.toLowerCase(),
@@ -12,6 +12,7 @@ import { nanoid } from '../../universal/id';
import { createRecipientAuthOptions } from '../../utils/document-auth';
import { type EnvelopeIdOptions, mapSecondaryIdToTemplateId } from '../../utils/envelope';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { assertCompatibleRecipientGrouping } from '../signature-level/assert-compatible-recipient-grouping';
import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role';
export type SetTemplateRecipientsOptions = {
@@ -68,6 +69,13 @@ export const setTemplateRecipients = async ({ userId, teamId, id, recipients }:
});
}
// This route replaces the whole recipient set, so the payload is the
// resulting state.
assertCompatibleRecipientGrouping({
signatureLevel: envelope.signatureLevel,
recipients,
});
const normalizedRecipients = recipients.map((recipient) => {
// Force replace any changes to the name or email of the direct recipient.
if (envelope.directLink && recipient.id === envelope.directLink.directTemplateRecipientId) {
@@ -14,6 +14,7 @@ import { mapFieldToLegacyField } from '../../utils/fields';
import { canRecipientBeModified } from '../../utils/recipients';
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { assertCompatibleRecipientGrouping } from '../signature-level/assert-compatible-recipient-grouping';
import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role';
export interface UpdateEnvelopeRecipientsOptions {
@@ -99,6 +100,17 @@ export const updateEnvelopeRecipients = async ({
});
}
// Grouping is a property of the whole recipient set, so check the state the
// envelope will be left in once these updates are applied.
assertCompatibleRecipientGrouping({
signatureLevel: envelope.signatureLevel,
recipients: envelope.recipients.map((existingRecipient) => {
const update = recipients.find((recipient) => recipient.id === existingRecipient.id);
return update ? { ...existingRecipient, ...update } : existingRecipient;
}),
});
const recipientsToUpdate = recipients.map((recipient) => {
const originalRecipient = envelope.recipients.find((existingRecipient) => existingRecipient.id === recipient.id);