mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
fix: resend document api v1 filtering logic (#1888)
The resend document API was not working correctly when filtering recipients. The query was filtering recipients at the database level, which could result in an empty recipients array being returned even when the document had recipients. This prevented the API from properly identifying which recipients needed reminder emails.
This commit is contained in:
223
packages/lib/server-only/document/resend-document.ts
Normal file
223
packages/lib/server-only/document/resend-document.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentStatus, OrganisationType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import {
|
||||
RECIPIENT_ROLES_DESCRIPTION,
|
||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||
} from '@documenso/lib/constants/recipient-roles';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export type ResendDocumentOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
recipients: number[];
|
||||
teamId: number;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const resendDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
recipients,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
}: ResendDocumentOptions): Promise<void> => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findUnique({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
teamEmail: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const customEmail = document?.documentMeta;
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
if (document.status === DocumentStatus.DRAFT) {
|
||||
throw new Error('Can not send draft document');
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
throw new Error('Can not send completed document');
|
||||
}
|
||||
|
||||
const recipientsToRemind = document.recipients.filter(
|
||||
(recipient) =>
|
||||
recipients.includes(recipient.id) && recipient.signingStatus === SigningStatus.NOT_SIGNED,
|
||||
);
|
||||
|
||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).recipientSigningRequest;
|
||||
|
||||
if (!isRecipientSigningRequestEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, settings, organisationType } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
recipientsToRemind.map(async (recipient) => {
|
||||
if (recipient.role === RecipientRole.CC) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
|
||||
const { email, name } = recipient;
|
||||
const selfSigner = email === user.email;
|
||||
|
||||
const recipientActionVerb = i18n
|
||||
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
|
||||
.toLowerCase();
|
||||
|
||||
let emailMessage = customEmail?.message || '';
|
||||
let emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} this document`);
|
||||
|
||||
if (selfSigner) {
|
||||
emailMessage = i18n._(
|
||||
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
|
||||
);
|
||||
emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} your document`);
|
||||
}
|
||||
|
||||
if (organisationType === OrganisationType.ORGANISATION) {
|
||||
emailSubject = i18n._(
|
||||
msg`Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`,
|
||||
);
|
||||
emailMessage =
|
||||
customEmail?.message ||
|
||||
i18n._(
|
||||
msg`${user.name || user.email} on behalf of "${document.team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||
);
|
||||
}
|
||||
|
||||
const customEmailTemplate = {
|
||||
'signer.name': name,
|
||||
'signer.email': email,
|
||||
'document.name': document.title,
|
||||
};
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
|
||||
|
||||
const template = createElement(DocumentInviteEmailTemplate, {
|
||||
documentName: document.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail:
|
||||
organisationType === OrganisationType.ORGANISATION
|
||||
? document.team?.teamEmail?.email || user.email
|
||||
: user.email,
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
|
||||
role: recipient.role,
|
||||
selfSigner,
|
||||
organisationType,
|
||||
teamName: document.team?.name,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: customEmail?.subject
|
||||
? renderCustomEmailTemplate(
|
||||
i18n._(msg`Reminder: ${customEmail.subject}`),
|
||||
customEmailTemplate,
|
||||
)
|
||||
: emailSubject,
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientRole: recipient.role,
|
||||
recipientId: recipient.id,
|
||||
isResending: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user