feat: signing order (#1290)

Adds the ability to specify an optional signing order for documents.
When specified a document will be considered sequential with recipients
only being allowed to sign in the order that they were specified in.
This commit is contained in:
Ephraim Duncan
2024-09-16 12:36:45 +00:00
committed by GitHub
parent 357bdd374f
commit 3d644db286
66 changed files with 1999 additions and 606 deletions

View File

@@ -2,11 +2,18 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import {
DocumentSigningOrder,
DocumentStatus,
RecipientRole,
SendStatus,
SigningStatus,
} from '@documenso/prisma/client';
import { WebhookTriggerEvents } from '@documenso/prisma/client';
import { jobs } from '../../jobs/client';
import type { TRecipientActionAuth } from '../../types/document-auth';
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { sendPendingEmail } from './send-pending-email';
@@ -29,6 +36,7 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
},
},
include: {
documentMeta: true,
Recipient: {
where: {
token,
@@ -59,6 +67,16 @@ export const completeDocumentWithToken = async ({
throw new Error(`Recipient ${recipient.id} has already signed`);
}
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
if (!isRecipientsTurn) {
throw new Error(
`Recipient ${recipient.id} attempted to complete the document before it was their turn`,
);
}
}
const fields = await prisma.field.findMany({
where: {
documentId: document.id,
@@ -120,17 +138,48 @@ export const completeDocumentWithToken = async ({
});
});
const pendingRecipients = await prisma.recipient.count({
const pendingRecipients = await prisma.recipient.findMany({
select: {
id: true,
signingOrder: true,
},
where: {
documentId: document.id,
signingStatus: {
not: SigningStatus.SIGNED,
},
role: {
not: RecipientRole.CC,
},
},
// Composite sort so our next recipient is always the one with the lowest signing order or id
// if there is a tie.
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
});
if (pendingRecipients > 0) {
if (pendingRecipients.length > 0) {
await sendPendingEmail({ documentId, recipientId: recipient.id });
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
const [nextRecipient] = pendingRecipients;
await prisma.$transaction(async (tx) => {
await tx.recipient.update({
where: { id: nextRecipient.id },
data: { sendStatus: SendStatus.SENT },
});
await jobs.triggerJob({
name: 'send.signing.requested.email',
payload: {
userId: document.userId,
documentId: document.id,
recipientId: nextRecipient.id,
requestMetadata,
},
});
});
}
}
const haveAllRecipientsSigned = await prisma.document.findFirst({
@@ -138,7 +187,7 @@ export const completeDocumentWithToken = async ({
id: document.id,
Recipient: {
every: {
signingStatus: SigningStatus.SIGNED,
OR: [{ signingStatus: SigningStatus.SIGNED }, { role: RecipientRole.CC }],
},
},
},

View File

@@ -3,7 +3,13 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, RecipientRole, SendStatus, SigningStatus } from '@documenso/prisma/client';
import {
DocumentSigningOrder,
DocumentStatus,
RecipientRole,
SendStatus,
SigningStatus,
} from '@documenso/prisma/client';
import { WebhookTriggerEvents } from '@documenso/prisma/client';
import { jobs } from '../../jobs/client';
@@ -57,7 +63,9 @@ export const sendDocument = async ({
}),
},
include: {
Recipient: true,
Recipient: {
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
},
documentMeta: true,
documentData: true,
},
@@ -75,6 +83,21 @@ export const sendDocument = async ({
throw new Error('Can not send completed document');
}
const signingOrder = document.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
let recipientsToNotify = document.Recipient;
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
// Get the currently active recipient.
recipientsToNotify = document.Recipient.filter(
(r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC,
).slice(0, 1);
// Secondary filter so we aren't resending if the current active recipient has already
// received the document.
recipientsToNotify.filter((r) => r.sendStatus !== SendStatus.SENT);
}
const { documentData } = document;
if (!documentData.data) {
@@ -135,7 +158,7 @@ export const sendDocument = async ({
if (sendEmail) {
await Promise.all(
document.Recipient.map(async (recipient) => {
recipientsToNotify.map(async (recipient) => {
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
return;
}