Files

209 lines
5.8 KiB
TypeScript

import { prisma } from '@documenso/prisma';
import {
DocumentSigningOrder,
DocumentStatus,
EnvelopeType,
RecipientRole,
SendStatus,
SigningStatus,
} from '@prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { jobs } from '../../jobs/client';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { isTspEnvelope } from '../../types/signature-level';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { mapSecondaryIdToDocumentId } from '../../utils/envelope';
import { getRecipientsInActiveSigningStep, isRecipientTurnBySigningOrder } from '../../utils/recipient-groups';
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
export type AdminDeleteRecipientOptions = {
recipientId: number;
reason: string;
requestMetadata: ApiRequestMetadata;
};
/**
* Unlike `deleteEnvelopeRecipient` this is not team scoped and allows removing
* recipients who have opened the document or inserted fields, as long as they
* have not signed.
*/
export const adminDeleteRecipient = async ({ recipientId, reason, requestMetadata }: AdminDeleteRecipientOptions) => {
const recipient = await prisma.recipient.findFirst({
where: {
id: recipientId,
},
include: {
envelope: {
select: {
id: true,
secondaryId: true,
type: true,
status: true,
signatureLevel: true,
completedAt: true,
userId: true,
documentMeta: {
select: {
signingOrder: true,
},
},
},
},
},
});
if (!recipient) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Recipient not found',
});
}
const { envelope } = recipient;
if (envelope.type !== EnvelopeType.DOCUMENT) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Recipient does not belong to a document',
});
}
if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document already complete',
});
}
if (recipient.signingStatus === SigningStatus.SIGNED) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Recipient has already completed signing',
});
}
// The TSP (AES/QES) lock protects WYSIWYS and is not bypassed for admins.
await assertEnvelopeMutable(envelope);
const deletedRecipient = await prisma.$transaction(async (tx) => {
await assertEnvelopeMutable(envelope, tx);
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
envelopeId: envelope.id,
metadata: requestMetadata,
data: {
recipientEmail: recipient.email,
recipientName: recipient.name,
recipientId: recipient.id,
recipientRole: recipient.role,
removedByAdmin: true,
reason,
},
}),
});
return await tx.recipient.delete({
where: {
id: recipientId,
},
});
});
const shouldNotifyRecipient = recipient.sendStatus === SendStatus.SENT && recipient.role !== RecipientRole.CC;
await jobs.triggerJob({
name: 'send.admin.recipient.removed.emails',
payload: {
envelopeId: envelope.id,
recipientEmail: recipient.email,
recipientName: recipient.name,
notifyRecipient: shouldNotifyRecipient,
},
});
// The removed recipient may have been the only thing blocking completion or
// the next sequential step, so seal or advance as the signing flow would.
if (envelope.status !== DocumentStatus.PENDING) {
return deletedRecipient;
}
const documentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
const remainingRecipients = await prisma.recipient.findMany({
where: {
envelopeId: envelope.id,
},
select: {
id: true,
role: true,
signingOrder: true,
signingStatus: true,
sendStatus: true,
},
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
});
const signableRecipients = remainingRecipients.filter((recipient) => recipient.role !== RecipientRole.CC);
// A document left with no signers stays pending rather than sealing with no signatures.
const haveAllRecipientsSigned =
signableRecipients.length > 0 &&
signableRecipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED);
if (haveAllRecipientsSigned) {
await jobs.triggerJob({
name: 'internal.seal-document',
payload: {
documentId,
requestMetadata: requestMetadata.requestMetadata,
},
});
return deletedRecipient;
}
if (envelope.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL) {
return deletedRecipient;
}
const recipientsToSend = getRecipientsInActiveSigningStep(remainingRecipients, {
strictlySequential: isTspEnvelope(envelope),
}).filter(
(recipient) =>
recipient.sendStatus !== SendStatus.SENT &&
isRecipientTurnBySigningOrder(remainingRecipients, recipient, { strictlySequential: isTspEnvelope(envelope) }),
);
if (recipientsToSend.length === 0) {
return deletedRecipient;
}
await prisma.recipient.updateMany({
where: {
id: {
in: recipientsToSend.map((recipient) => recipient.id),
},
},
data: {
sendStatus: SendStatus.SENT,
sentAt: new Date(),
},
});
await Promise.allSettled(
recipientsToSend.map(async (recipient) =>
jobs.triggerJob({
name: 'send.signing.requested.email',
payload: {
userId: envelope.userId,
documentId,
recipientId: recipient.id,
requestMetadata: requestMetadata.requestMetadata,
},
}),
),
);
return deletedRecipient;
};