import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { prisma } from '@documenso/prisma'; import { DocumentSigningOrder, DocumentStatus, RecipientRole, SendStatus, SigningStatus, WebhookTriggerEvents, } from '@documenso/prisma/client'; import { jobs } from '../../jobs/client'; import type { TRecipientActionAuth } from '../../types/document-auth'; import { ZWebhookDocumentSchema, mapDocumentToWebhookDocumentPayload, } from '../../types/webhook-payload'; import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; import { sendPendingEmail } from './send-pending-email'; export type CompleteDocumentWithTokenOptions = { token: string; documentId: number; userId?: number; authOptions?: TRecipientActionAuth; requestMetadata?: RequestMetadata; nextSigner?: { email: string; name: string; }; }; const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => { return await prisma.document.findFirstOrThrow({ where: { id: documentId, recipients: { some: { token, }, }, }, include: { documentMeta: true, recipients: { where: { token, }, }, }, }); }; export const delegateNextSigner = async ({ documentId, currentRecipientId, nextSigner, }: { documentId: number; currentRecipientId: number; nextSigner: { email: string; name: string }; }) => { const document = await prisma.document.findUnique({ where: { id: documentId }, include: { recipients: { orderBy: [{ signingOrder: 'asc' }, { id: 'asc' }], }, }, }); if (!document) { throw new Error('Document not found'); } const currentRecipient = document.recipients.find((r) => r.id === currentRecipientId); const nextRecipient = document.recipients.find( (r) => r.signingOrder === (currentRecipient?.signingOrder ?? 0) + 1, ); if (!nextRecipient) { throw new Error('Next recipient not found'); } await prisma.recipient.update({ where: { id: nextRecipient.id }, data: { email: nextSigner.email, name: nextSigner.name, }, }); return nextRecipient; }; export const completeDocumentWithToken = async ({ token, documentId, requestMetadata, nextSigner, }: CompleteDocumentWithTokenOptions) => { console.log('completeDocumentWithToken == document-router', token, documentId, nextSigner); const document = await getDocument({ token, documentId }); if (document.status !== DocumentStatus.PENDING) { throw new Error(`Document ${document.id} must be pending`); } if (document.recipients.length === 0) { throw new Error(`Document ${document.id} has no recipient with token ${token}`); } const [recipient] = document.recipients; if (recipient.signingStatus === SigningStatus.SIGNED) { 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, recipientId: recipient.id, }, }); if (fieldsContainUnsignedRequiredField(fields)) { throw new Error(`Recipient ${recipient.id} has unsigned fields`); } // Document reauth for completing documents is currently not required. // const { derivedRecipientActionAuth } = extractDocumentAuthMethods({ // documentAuth: document.authOptions, // recipientAuth: recipient.authOptions, // }); // const isValid = await isRecipientAuthorized({ // type: 'ACTION', // document: document, // recipient: recipient, // userId, // authOptions, // }); // if (!isValid) { // throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid authentication values'); // } if ( nextSigner && document.documentMeta?.modifyNextSigner && document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL ) { console.log('delegateNextSigner == document-router', document.id, recipient.id, nextSigner); await delegateNextSigner({ documentId: document.id, currentRecipientId: recipient.id, nextSigner, }); } await prisma.$transaction(async (tx) => { await tx.recipient.update({ where: { id: recipient.id, }, data: { signingStatus: SigningStatus.SIGNED, signedAt: new Date(), }, }); await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, documentId: document.id, user: { name: recipient.name, email: recipient.email, }, requestMetadata, data: { recipientEmail: recipient.email, recipientName: recipient.name, recipientId: recipient.id, recipientRole: recipient.role, // actionAuth: derivedRecipientActionAuth || undefined, }, }), }); }); await jobs.triggerJob({ name: 'send.recipient.signed.email', payload: { documentId: document.id, recipientId: recipient.id, }, }); 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.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({ where: { id: document.id, recipients: { every: { OR: [{ signingStatus: SigningStatus.SIGNED }, { role: RecipientRole.CC }], }, }, }, }); if (haveAllRecipientsSigned) { await jobs.triggerJob({ name: 'internal.seal-document', payload: { documentId: document.id, requestMetadata, }, }); } const updatedDocument = await prisma.document.findFirstOrThrow({ where: { id: document.id, }, include: { documentMeta: true, recipients: true, }, }); await triggerWebhook({ event: WebhookTriggerEvents.DOCUMENT_SIGNED, data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)), userId: updatedDocument.userId, teamId: updatedDocument.teamId ?? undefined, }); };